From 9abe7116ecb8b5fe21f539bfb0f683db946cd027 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Sun, 7 Dec 2025 18:19:36 -0300 Subject: [PATCH 01/61] Introduce VersionSpec, a model meant for advanced version handling for applicabilities --- .../AppliesTo/ApplicableToYamlConverter.cs | 92 +++++++ .../Serialization/SourceGenerationContext.cs | 1 + src/Elastic.Documentation/VersionSpec.cs | 241 ++++++++++++++++++ 3 files changed, 334 insertions(+) create mode 100644 src/Elastic.Documentation/VersionSpec.cs diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs index 1017207e1..2b6269034 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs @@ -256,10 +256,102 @@ private static bool TryGetApplicabilityOverTime(Dictionary dict if (target is null || (target is string s && string.IsNullOrWhiteSpace(s))) availability = AppliesCollection.GenerallyAvailable; else if (target is string stackString) + { availability = AppliesCollection.TryParse(stackString, diagnostics, out var a) ? a : null; + + if (availability is not null) + ValidateApplicabilityCollection(key, availability, diagnostics); + } return availability is not null; } + private static void ValidateApplicabilityCollection(string key, AppliesCollection collection, List<(Severity, string)> diagnostics) + { + var items = collection.ToList(); + + // Rule: Only one version declaration per lifecycle + var lifecycleGroups = items.GroupBy(a => a.Lifecycle).ToList(); + foreach (var group in lifecycleGroups) + { + var lifecycleVersionedItems = group.Where(a => a.Version is not null && + a.Version != AllVersionsSpec.Instance).ToList(); + if (lifecycleVersionedItems.Count > 1) + { + diagnostics.Add((Severity.Warning, + $"Key '{key}': Multiple version declarations for {group.Key} lifecycle. Only one version per lifecycle is allowed.")); + } + } + + // Rule: Only one item per key can use greater-than syntax + var greaterThanItems = items.Where(a => + a.Version is VersionSpec spec && spec.Kind == VersionSpecKind.GreaterThanOrEqual && + a.Version != AllVersionsSpec.Instance).ToList(); + + if (greaterThanItems.Count > 1) + { + diagnostics.Add((Severity.Warning, + $"Key '{key}': Multiple items use greater-than-or-equal syntax. Only one item per key can use this syntax.")); + } + + // Rule: In a range, the first version must be less than or equal the last version + foreach (var item in items) + { + if (item.Version is { Kind: VersionSpecKind.Range } spec) + { + if (spec.Min.CompareTo(spec.Max!) > 0) + { + diagnostics.Add((Severity.Warning, + $"Key '{key}', {item.Lifecycle}: Range has first version ({spec.Min.Major}.{spec.Min.Minor}) greater than last version ({spec.Max!.Major}.{spec.Max.Minor}).")); + } + } + } + + // Rule: No overlapping version ranges for the same key + var versionedItems = items.Where(a => a.Version is not null && + a.Version != AllVersionsSpec.Instance).ToList(); + + for (var i = 0; i < versionedItems.Count; i++) + { + for (var j = i + 1; j < versionedItems.Count; j++) + { + if (CheckVersionOverlap(versionedItems[i].Version!, versionedItems[j].Version!, out var overlapMsg)) + { + diagnostics.Add((Severity.Warning, + $"Key '{key}': Overlapping versions between {versionedItems[i].Lifecycle} and {versionedItems[j].Lifecycle}. {overlapMsg}")); + } + } + } + } + + private static bool CheckVersionOverlap(VersionSpec v1, VersionSpec v2, out string message) + { + message = string.Empty; + + // Get the effective ranges for each version spec + // For GreaterThanOrEqual: [min, infinity) + // For Range: [min, max] + // For Exact: [exact, exact] + + var (v1Min, v1Max) = GetEffectiveRange(v1); + var (v2Min, v2Max) = GetEffectiveRange(v2); + + var overlaps = v1Min.CompareTo(v2Max ?? new SemVersion(9999, 9999, 9999)) <= 0 && + v2Min.CompareTo(v1Max ?? new SemVersion(9999, 9999, 9999)) <= 0; + + if (overlaps) + message = $"Version ranges overlap."; + + return overlaps; + } + + private static (SemVersion min, SemVersion? max) GetEffectiveRange(VersionSpec spec) => spec.Kind switch + { + VersionSpecKind.Exact => (spec.Min, spec.Min), + VersionSpecKind.Range => (spec.Min, spec.Max), + VersionSpecKind.GreaterThanOrEqual => (spec.Min, null), + _ => throw new ArgumentOutOfRangeException(nameof(spec), spec.Kind, "Unknown VersionSpecKind") + }; + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => serializer.Invoke(value, type); } diff --git a/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs b/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs index 7a97730a3..cde2ac16e 100644 --- a/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs +++ b/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs @@ -28,5 +28,6 @@ namespace Elastic.Documentation.Serialization; [JsonSerializable(typeof(Applicability))] [JsonSerializable(typeof(ProductLifecycle))] [JsonSerializable(typeof(SemVersion))] +[JsonSerializable(typeof(VersionSpec))] [JsonSerializable(typeof(string[]))] public sealed partial class SourceGenerationContext : JsonSerializerContext; diff --git a/src/Elastic.Documentation/VersionSpec.cs b/src/Elastic.Documentation/VersionSpec.cs new file mode 100644 index 000000000..2cfb66774 --- /dev/null +++ b/src/Elastic.Documentation/VersionSpec.cs @@ -0,0 +1,241 @@ +// 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.Diagnostics.CodeAnalysis; + +namespace Elastic.Documentation; + +public sealed class AllVersionsSpec : VersionSpec +{ + private static readonly SemVersion AllVersionsSemVersion = new(9999, 9999, 9999); + + private AllVersionsSpec() : base(AllVersionsSemVersion, null, VersionSpecKind.GreaterThanOrEqual) + { + } + + public static AllVersionsSpec Instance { get; } = new(); + + public override string ToString() => "all"; +} + +public enum VersionSpecKind +{ + GreaterThanOrEqual, // x.x, x.x+, x.x.x, x.x.x+ + Range, // x.x-y.y, x.x.x-y.y.y + Exact // =x.x, =x.x.x +} + +/// +/// Represents a version specification that can be a single version with greater-than-or-equal semantics, +/// a range of versions, or an exact version match. +/// +public class VersionSpec : IComparable, IEquatable +{ + /// + /// The minimum version (or the exact version for Exact kind). + /// + public SemVersion Min { get; } + + /// + /// The maximum version for ranges. Null for GreaterThanOrEqual and Exact kinds. + /// + public SemVersion? Max { get; } + + /// + /// The kind of version specification. + /// + public VersionSpecKind Kind { get; } + + // Internal constructor to prevent direct instantiation outside of TryParse + // except for AllVersionsSpec which needs to inherit from this class + protected VersionSpec(SemVersion min, SemVersion? max, VersionSpecKind kind) + { + Min = min; + Max = max; + Kind = kind; + } + + /// + /// Tries to parse a version specification string. + /// Supports: x.x, x.x+, x.x.x, x.x.x+ (gte), x.x-y.y (range), =x.x (exact) + /// + public static bool TryParse(string? input, [NotNullWhen(true)] out VersionSpec? spec) + { + spec = null; + + if (string.IsNullOrWhiteSpace(input)) + return false; + + var trimmed = input.Trim(); + + // Check for exact syntax: =x.x or =x.x.x + if (trimmed.StartsWith('=')) + { + var versionPart = trimmed[1..]; + if (!TryParseVersion(versionPart, out var version)) + return false; + + spec = new(version, null, VersionSpecKind.Exact); + return true; + } + + // Check for range syntax: x.x-y.y or x.x.x-y.y.y + var dashIndex = FindRangeSeparator(trimmed); + if (dashIndex > 0) + { + var minPart = trimmed[..dashIndex]; + var maxPart = trimmed[(dashIndex + 1)..]; + + if (!TryParseVersion(minPart, out var minVersion) || + !TryParseVersion(maxPart, out var maxVersion)) + return false; + + spec = new(minVersion, maxVersion, VersionSpecKind.Range); + return true; + } + + // Otherwise, it's greater-than-or-equal syntax + // Strip trailing + if present + var versionString = trimmed.EndsWith('+') ? trimmed[..^1] : trimmed; + + if (!TryParseVersion(versionString, out var gteVersion)) + return false; + + spec = new(gteVersion, null, VersionSpecKind.GreaterThanOrEqual); + return true; + } + + /// + /// Finds the position of the dash separator in a range specification. + /// Returns -1 if no valid range separator is found. + /// + private static int FindRangeSeparator(string input) + { + // Look for a dash that's not part of a prerelease version + // We need to distinguish between "9.0-9.1" (range) and "9.0-alpha" (prerelease) + // Strategy: Find dashes and check if what follows looks like a version number + + for (var i = 0; i < input.Length; i++) + { + if (input[i] == '-') + { + // Check if there's content before and after the dash + if (i == 0 || i == input.Length - 1) + continue; + + // Check if the character after dash is a digit (indicating a version) + if (i + 1 < input.Length && char.IsDigit(input[i + 1])) + { + // Also verify that what comes before looks like a version + var beforeDash = input[..i]; + if (TryParseVersion(beforeDash, out _)) + return i; + } + } + } + + return -1; + } + + /// + /// Tries to parse a version string, normalizing minor versions to include patch 0. + /// + private static bool TryParseVersion(string input, [NotNullWhen(true)] out SemVersion? version) + { + version = null; + + if (string.IsNullOrWhiteSpace(input)) + return false; + + var trimmed = input.Trim(); + + // Try to parse as-is first + if (SemVersion.TryParse(trimmed, out version)) + return true; + + // If that fails, try appending .0 to support minor version format (e.g., "9.2" -> "9.2.0") + if (SemVersion.TryParse(trimmed + ".0", out version)) + return true; + + return false; + } + + /// + /// Returns the canonical string representation of this version spec. + /// Format: "9.2+" for GreaterThanOrEqual, "9.0-9.1" for Range, "=9.2" for Exact + /// + public override string ToString() => Kind switch + { + VersionSpecKind.Exact => $"={Min.Major}.{Min.Minor}", + VersionSpecKind.Range => $"{Min.Major}.{Min.Minor}-{Max!.Major}.{Max.Minor}", + VersionSpecKind.GreaterThanOrEqual => $"{Min.Major}.{Min.Minor}+", + _ => throw new ArgumentOutOfRangeException(nameof(Kind), Kind, null) + }; + + /// + /// Compares this VersionSpec to another for sorting. + /// Uses Max for ranges, otherwise uses Min. + /// + public int CompareTo(VersionSpec? other) + { + if (other is null) + return 1; + + // For sorting, we want to compare the "highest" version in each spec + var thisCompareVersion = Kind == VersionSpecKind.Range && Max is not null ? Max : Min; + var otherCompareVersion = other.Kind == VersionSpecKind.Range && other.Max is not null ? other.Max : other.Min; + + return thisCompareVersion.CompareTo(otherCompareVersion); + } + + /// + /// Checks if this VersionSpec is equal to another. + /// + public bool Equals(VersionSpec? other) + { + if (other is null) + return false; + + if (ReferenceEquals(this, other)) + return true; + + return Kind == other.Kind && Min.Equals(other.Min) && + (Max?.Equals(other.Max) ?? (other.Max is null)); + } + + public override bool Equals(object? obj) => obj is VersionSpec other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(Kind, Min, Max); + + public static bool operator ==(VersionSpec? left, VersionSpec? right) + { + if (left is null) + return right is null; + return left.Equals(right); + } + + public static bool operator !=(VersionSpec? left, VersionSpec? right) => !(left == right); + + public static bool operator <(VersionSpec? left, VersionSpec? right) => + left is null ? right is not null : left.CompareTo(right) < 0; + + public static bool operator <=(VersionSpec? left, VersionSpec? right) => + left is null || left.CompareTo(right) <= 0; + + public static bool operator >(VersionSpec? left, VersionSpec? right) => + left is not null && left.CompareTo(right) > 0; + + public static bool operator >=(VersionSpec? left, VersionSpec? right) => + left is null ? right is null : left.CompareTo(right) >= 0; + + /// + /// Explicit conversion from string to VersionSpec + /// + public static explicit operator VersionSpec(string s) + { + if (TryParse(s, out var spec)) + return spec!; + throw new ArgumentException($"'{s}' is not a valid version specification string."); + } +} From 73e8dd79f96915e550c41a2e2a1274754f437b61 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 8 Dec 2025 03:43:55 -0300 Subject: [PATCH 02/61] Apply usage of VersionSpec in Applicability --- docs/_snippets/applies_to-version.md | 14 +- docs/syntax/applies.md | 91 ++++++++- .../Elasticsearch/OpenApiDocumentExporter.cs | 6 +- .../AppliesTo/Applicability.cs | 20 +- .../AppliesTo/ApplicabilitySelector.cs | 10 +- .../AppliesTo/ApplicableTo.cs | 4 +- .../AppliesTo/ApplicableToJsonConverter.cs | 4 +- .../Myst/Components/ApplicabilityRenderer.cs | 177 +++++++++++++----- .../Components/ApplicableToComponent.cshtml | 2 +- ...ApplicableToJsonConverterRoundTripTests.cs | 92 ++++----- ...icableToJsonConverterSerializationTests.cs | 54 +++--- .../ProductApplicabilityToStringTests.cs | 2 +- .../Directives/ApplicabilitySwitchTests.cs | 11 +- ...DocumentationDocumentSerializationTests.cs | 40 ++-- .../Applicability/ApplicableToComponent.fs | 8 +- tests/authoring/Inline/AppliesToRole.fs | 4 +- 16 files changed, 357 insertions(+), 182 deletions(-) diff --git a/docs/_snippets/applies_to-version.md b/docs/_snippets/applies_to-version.md index f98f758d0..e5e70f63b 100644 --- a/docs/_snippets/applies_to-version.md +++ b/docs/_snippets/applies_to-version.md @@ -1,10 +1,16 @@ `applies_to` accepts the following version formats: -* `Major.Minor` -* `Major.Minor.Patch` +* **Greater than or equal to**: `x.x+`, `x.x`, `x.x.x+`, `x.x.x` (default behavior when no operator specified) +* **Range (inclusive)**: `x.x-y.y`, `x.x.x-y.y.y`, `x.x-y.y.y`, `x.x.x-y.y` +* **Exact version**: `=x.x`, `=x.x.x` -Regardless of the version format used in the source file, the version number is always rendered in the `Major.Minor.Patch` format. +**Version Display:** + +- Versions are always displayed as **Major.Minor** (e.g., `9.1`) in badges, regardless of the format used in source files. +- Each version represents the **latest patch** of that minor version (e.g., `9.1` means 9.1.0, 9.1.1, 9.1.6, etc.). +- The `+` symbol indicates "this version and later" (e.g., `9.1+` means 9.1.0 and all subsequent releases). +- Ranges show both versions (e.g., `9.0-9.2`) when both are released, or convert to `+` format if the end version is unreleased. :::{note} -**Automatic Version Sorting**: When you specify multiple versions for the same product, the build system automatically sorts them in descending order (highest version first) regardless of the order you write them in the source file. For example, `stack: ga 8.18.6, ga 9.1.2, ga 8.19.2, ga 9.0.6` will be displayed as `stack: ga 9.1.2, ga 9.0.6, ga 8.19.2, ga 8.18.6`. Items without versions (like `ga` without a version or `all`) are sorted last. +**Automatic Version Sorting**: When you specify multiple versions for the same product, the build system automatically sorts them in descending order (highest version first) regardless of the order you write them in the source file. For example, `stack: ga 9.1, beta 9.0, preview 8.18` will be displayed with the highest priority lifecycle and version first. Items without versions are sorted last. ::: \ No newline at end of file diff --git a/docs/syntax/applies.md b/docs/syntax/applies.md index c6621e483..59c33d720 100644 --- a/docs/syntax/applies.md +++ b/docs/syntax/applies.md @@ -29,6 +29,41 @@ Where: - The lifecycle is mandatory. - The version is optional. +### Version Syntax + +Versions can be specified using several formats to indicate different applicability scenarios: + +| Description | Syntax | Example | Badge Display | +|:------------|:-------|:--------|:--------------| +| **Greater than or equal to** (default) | `x.x+` `x.x` `x.x.x+` `x.x.x` | `ga 9.1` or `ga 9.1+` | `9.1+` | +| **Range** (inclusive) | `x.x-y.y` `x.x.x-y.y.y` | `preview 9.0-9.2` | `9.0-9.2` or `9.0+`* | +| **Exact version** | `=x.x` `=x.x.x` | `beta =9.1` | `9.1` | + +\* Range display depends on release status of the second version. + +**Important notes:** + +- Versions are always displayed as **Major.Minor** (e.g., `9.1`) in badges, regardless of whether you specify patch versions in the source. +- Each version statement corresponds to the **latest patch** of the specified minor version (e.g., `9.1` represents 9.1.0, 9.1.1, 9.1.6, etc.). +- When critical patch-level differences exist, use plain text descriptions alongside the badge rather than specifying patch versions. + +### Version Validation Rules + +The build process enforces the following validation rules: + +- **One version per lifecycle**: Each lifecycle (GA, Preview, Beta, etc.) can only have one version declaration. + - ✅ `stack: ga 9.2+, beta 9.0-9.1` + - ❌ `stack: ga 9.2, ga 9.3` +- **One "greater than" per key**: Only one lifecycle per product key can use the `+` (greater than or equal to) syntax. + - ✅ `stack: ga 9.2+, beta 9.0-9.1` + - ❌ `stack: ga 9.2+, beta 9.0+` +- **Valid range order**: In ranges, the first version must be less than or equal to the second version. + - ✅ `stack: preview 9.0-9.2` + - ❌ `stack: preview 9.2-9.0` +- **No version overlaps**: Versions for the same key cannot overlap (ranges are inclusive). + - ✅ `stack: ga 9.2+, beta 9.0-9.1` + - ❌ `stack: ga 9.2+, beta 9.0-9.2` + ### Page level Page level annotations are added in the YAML frontmatter, starting with the `applies_to` key and following the [key-value reference](#key-value-reference). For example: @@ -134,6 +169,22 @@ Use the following key-value reference to find the appropriate key and value for ## Examples +### Version Syntax Examples + +The following table demonstrates the various version syntax options and their rendered output: + +| Source Syntax | Description | Badge Display | Notes | +|:-------------|:------------|:--------------|:------| +| `stack: ga 9.1` | Greater than or equal to 9.1 | `Stack│9.1+` | Default behavior, equivalent to `9.1+` | +| `stack: ga 9.1+` | Explicit greater than or equal to | `Stack│9.1+` | Explicit `+` syntax | +| `stack: preview 9.0-9.2` | Range from 9.0 to 9.2 (inclusive) | `Stack│Preview 9.0-9.2` | Shows range if 9.2.0 is released | +| `stack: preview 9.0-9.3` | Range where end is unreleased | `Stack│Preview 9.0+` | Shows `+` if 9.3.0 is not released | +| `stack: beta =9.1` | Exact version 9.1 only | `Stack│Beta 9.1` | No `+` symbol for exact versions | +| `stack: ga 9.2+, beta 9.0-9.1` | Multiple lifecycles | `Stack│9.2+` | Only highest priority lifecycle shown | +| `stack: ga 9.3, beta 9.1+` | Unreleased GA with Preview | `Stack│Beta 9.1+` | Shows Beta when GA unreleased with 2+ lifecycles | +| `serverless: ga` | No version (base 99999) | `Serverless` | No version badge for unversioned products | +| `deployment:`
` ece: ga 9.0+` | Nested deployment syntax | `ECE│9.0+` | Deployment products shown separately | + ### Versioning examples Versioned products require a `version` tag to be used with the `lifecycle` tag: @@ -240,22 +291,46 @@ applies_to: ## Look and feel +### Version Syntax Demonstrations + +:::::{dropdown} New version syntax examples + +The following examples demonstrate the new version syntax capabilities: + +**Greater than or equal to:** +- {applies_to}`stack: ga 9.1` (implicit `+`) +- {applies_to}`stack: ga 9.1+` (explicit `+`) +- {applies_to}`stack: preview 9.0+` + +**Ranges:** +- {applies_to}`stack: preview 9.0-9.2` (range display when both released) +- {applies_to}`stack: beta 9.1-9.3` (converts to `+` if end unreleased) + +**Exact versions:** +- {applies_to}`stack: beta =9.1` (no `+` symbol) +- {applies_to}`stack: deprecated =9.0` + +**Multiple lifecycles:** +- {applies_to}`stack: ga 9.2+, beta 9.0-9.1` (shows highest priority) + +::::: + ### Block :::::{dropdown} Block examples ```{applies_to} -stack: preview 9.1 +stack: preview 9.1+ serverless: ga -apm_agent_dotnet: ga 1.0.0 -apm_agent_java: beta 1.0.0 -edot_dotnet: preview 1.0.0 +apm_agent_dotnet: ga 1.0+ +apm_agent_java: beta 1.0+ +edot_dotnet: preview 1.0+ edot_python: -edot_node: ga 1.0.0 -elasticsearch: preview 9.0.0 -security: removed 9.0.0 -observability: deprecated 9.0.0 +edot_node: ga 1.0+ +elasticsearch: preview 9.0+ +security: removed 9.0 +observability: deprecated 9.0+ ``` ::::: diff --git a/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs b/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs index b2c82a1ec..d94f5ca7c 100644 --- a/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs +++ b/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs @@ -209,7 +209,7 @@ private bool ShouldIncludeOperation(OpenApiOperation operation, string product) return true; // Could not parse version, safe to include // Get current version for the product - var versioningSystemId = product == "elasticsearch" + var versioningSystemId = product.Equals("elasticsearch", StringComparison.OrdinalIgnoreCase) ? VersioningSystemId.Stack : VersioningSystemId.Stack; // Both use Stack for now @@ -294,14 +294,14 @@ private static ProductLifecycle ParseLifecycle(string stateValue) /// /// Parses the version from "Added in X.Y.Z" pattern in the x-state string. /// - private static SemVersion? ParseVersion(string stateValue) + private static VersionSpec? ParseVersion(string stateValue) { var match = AddedInVersionRegex().Match(stateValue); if (!match.Success) return null; var versionString = match.Groups[1].Value; - return SemVersion.TryParse(versionString, out var version) ? version : null; + return VersionSpec.TryParse(versionString, out var version) ? version : null; } /// diff --git a/src/Elastic.Documentation/AppliesTo/Applicability.cs b/src/Elastic.Documentation/AppliesTo/Applicability.cs index b91fdb991..2d5abbff9 100644 --- a/src/Elastic.Documentation/AppliesTo/Applicability.cs +++ b/src/Elastic.Documentation/AppliesTo/Applicability.cs @@ -38,7 +38,7 @@ public static bool TryParse(string? value, IList<(Severity, string)> diagnostics return false; // Sort by version in descending order (the highest version first) - // Items without versions (AllVersions.Instance) are sorted last + // Items without versions (AllVersionsSpec.Instance) are sorted last var sortedApplications = applications.OrderDescending().ToArray(); availability = new AppliesCollection(sortedApplications); return true; @@ -98,12 +98,12 @@ public override string ToString() public record Applicability : IComparable, IComparable { public ProductLifecycle Lifecycle { get; init; } - public SemVersion? Version { get; init; } + public VersionSpec? Version { get; init; } public static Applicability GenerallyAvailable { get; } = new() { Lifecycle = ProductLifecycle.GenerallyAvailable, - Version = AllVersions.Instance + Version = AllVersionsSpec.Instance }; @@ -126,8 +126,8 @@ public string GetLifeCycleName() => /// public int CompareTo(Applicability? other) { - var xIsNonVersioned = Version is null || ReferenceEquals(Version, AllVersions.Instance); - var yIsNonVersioned = other?.Version is null || ReferenceEquals(other.Version, AllVersions.Instance); + var xIsNonVersioned = Version is null || ReferenceEquals(Version, AllVersionsSpec.Instance); + var yIsNonVersioned = other?.Version is null || ReferenceEquals(other.Version, AllVersionsSpec.Instance); if (xIsNonVersioned && yIsNonVersioned) return 0; @@ -158,7 +158,7 @@ public override string ToString() _ => throw new ArgumentOutOfRangeException() }; _ = sb.Append(lifecycle); - if (Version is not null && Version != AllVersions.Instance) + if (Version is not null && Version != AllVersionsSpec.Instance) _ = sb.Append(' ').Append(Version); return sb.ToString(); } @@ -224,10 +224,10 @@ public static bool TryParse(string? value, IList<(Severity, string)> diagnostics ? null : tokens[1] switch { - null => AllVersions.Instance, - "all" => AllVersions.Instance, - "" => AllVersions.Instance, - var t => SemVersionConverter.TryParse(t, out var v) ? v : null + null => AllVersionsSpec.Instance, + "all" => AllVersionsSpec.Instance, + "" => AllVersionsSpec.Instance, + var t => VersionSpec.TryParse(t, out var v) ? v : null }; availability = new Applicability { Version = version, Lifecycle = lifecycle }; return true; diff --git a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs index cb881fbf6..d090f2b02 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs @@ -30,25 +30,27 @@ public static Applicability GetPrimaryApplicability(IEnumerable a }; var availableApplicabilities = applicabilityList - .Where(a => a.Version is null || a.Version is AllVersions || a.Version <= currentVersion) + .Where(a => a.Version is null || a.Version is AllVersionsSpec || + (a.Version is VersionSpec vs && vs.Min <= currentVersion)) .ToList(); if (availableApplicabilities.Count != 0) { return availableApplicabilities - .OrderByDescending(a => a.Version ?? new SemVersion(0, 0, 0)) + .OrderByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) .ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999)) .First(); } var futureApplicabilities = applicabilityList - .Where(a => a.Version is not null && a.Version is not AllVersions && a.Version > currentVersion) + .Where(a => a.Version is not null && a.Version is not AllVersionsSpec && + a.Version is VersionSpec vs && vs.Min > currentVersion) .ToList(); if (futureApplicabilities.Count != 0) { return futureApplicabilities - .OrderBy(a => a.Version!.CompareTo(currentVersion)) + .OrderBy(a => a.Version!.Min.CompareTo(currentVersion)) .ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999)) .First(); } diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs index 5889a9964..94ff54d4c 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs @@ -64,9 +64,11 @@ public record ApplicableTo Product = AppliesCollection.GenerallyAvailable }; + private static readonly VersionSpec DefaultVersion = VersionSpec.TryParse("9.0", out var v) ? v! : AllVersionsSpec.Instance; + public static ApplicableTo Default { get; } = new() { - Stack = new AppliesCollection([new Applicability { Version = new SemVersion(9, 0, 0), Lifecycle = ProductLifecycle.GenerallyAvailable }]), + Stack = new AppliesCollection([new Applicability { Version = DefaultVersion, Lifecycle = ProductLifecycle.GenerallyAvailable }]), Serverless = ServerlessProjectApplicability.All }; diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs index d3779e525..c36e350dc 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs @@ -44,7 +44,7 @@ public class ApplicableToJsonConverter : JsonConverter string? type = null; string? subType = null; var lifecycle = ProductLifecycle.GenerallyAvailable; - SemVersion? version = null; + VersionSpec? version = null; while (reader.Read()) { @@ -72,7 +72,7 @@ public class ApplicableToJsonConverter : JsonConverter break; case "version": var versionStr = reader.GetString(); - if (versionStr != null && SemVersionConverter.TryParse(versionStr, out var v)) + if (versionStr != null && VersionSpecConverter.TryParse(versionStr, out var v)) version = v; break; } diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 8ffe34306..7984fe4a8 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -16,6 +16,7 @@ public record ApplicabilityRenderData( string Version, string TooltipText, string LifecycleClass, + string LifecycleName, bool ShowLifecycleName, bool ShowVersion, bool HasMultipleLifecycles = false @@ -29,19 +30,36 @@ public ApplicabilityRenderData RenderApplicability( { var lifecycleClass = applicability.GetLifeCycleName().ToLowerInvariant().Replace(" ", "-"); var lifecycleFull = GetLifecycleFullText(applicability.Lifecycle); - var realVersion = TryGetRealVersion(applicability, out var v) ? v : null; - var tooltipText = BuildTooltipText(applicability, applicabilityDefinition, versioningSystem, realVersion, lifecycleFull); - var badgeLifecycleText = BuildBadgeLifecycleText(applicability, versioningSystem, realVersion, allApplications); + var tooltipText = BuildTooltipText(applicability, applicabilityDefinition, versioningSystem, lifecycleFull); + var badgeLifecycleText = BuildBadgeLifecycleText(applicability, versioningSystem, allApplications); var showLifecycle = applicability.Lifecycle != ProductLifecycle.GenerallyAvailable && string.IsNullOrEmpty(badgeLifecycleText); - var showVersion = applicability.Version is not null and not AllVersions && versioningSystem.Current >= applicability.Version; - var version = applicability.Version?.ToString() ?? ""; + + // Determine if we should show version based on VersionSpec + var showVersion = false; + var versionDisplay = string.Empty; + + if (applicability.Version is not null && applicability.Version != AllVersionsSpec.Instance) + { + versionDisplay = GetBadgeVersionText(applicability.Version, versioningSystem); + showVersion = !string.IsNullOrEmpty(versionDisplay); + + // Special handling for Removed lifecycle - don't show + suffix + if (applicability.Lifecycle == ProductLifecycle.Removed && + applicability.Version.Kind == VersionSpecKind.GreaterThanOrEqual && + !string.IsNullOrEmpty(versionDisplay)) + { + versionDisplay = versionDisplay.TrimEnd('+'); + } + } + return new ApplicabilityRenderData( BadgeLifecycleText: badgeLifecycleText, - Version: version, + Version: versionDisplay, TooltipText: tooltipText, LifecycleClass: lifecycleClass, + LifecycleName: applicability.GetLifeCycleName(), ShowLifecycleName: showLifecycle, ShowVersion: showVersion ); @@ -54,9 +72,26 @@ public ApplicabilityRenderData RenderCombinedApplicability( AppliesCollection allApplications) { var applicabilityList = applicabilities.ToList(); - var primaryApplicability = ApplicabilitySelector.GetPrimaryApplicability(applicabilityList, versioningSystem.Current); - var primaryRenderData = RenderApplicability(primaryApplicability, applicabilityDefinition, versioningSystem, allApplications); + // Sort by lifecycle priority (GA > Beta > Preview > etc.) to determine display order + var sortedApplicabilities = applicabilityList + .OrderBy(a => GetLifecycleOrder(a.Lifecycle)) + .ThenByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) + .ToList(); + + var primaryLifecycle = sortedApplicabilities.First(); + + var primaryRender = RenderApplicability(primaryLifecycle, applicabilityDefinition, versioningSystem, allApplications); + + // If the primary lifecycle returns an empty badge text (indicating "use previous lifecycle") + // and we have multiple lifecycles, use the next lifecycle in priority order + var applicabilityToDisplay = string.IsNullOrEmpty(primaryRender.BadgeLifecycleText) && + string.IsNullOrEmpty(primaryRender.Version) && + sortedApplicabilities.Count >= 2 + ? sortedApplicabilities[1] + : primaryLifecycle; + + var primaryRenderData = RenderApplicability(applicabilityToDisplay, applicabilityDefinition, versioningSystem, allApplications); var combinedTooltip = BuildCombinedTooltipText(applicabilityList, applicabilityDefinition, versioningSystem); // Check if there are multiple different lifecycles @@ -70,7 +105,6 @@ public ApplicabilityRenderData RenderCombinedApplicability( }; } - private static string BuildCombinedTooltipText( List applicabilities, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, @@ -80,17 +114,17 @@ private static string BuildCombinedTooltipText( // 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)) + .OrderByDescending(a => a.Version is null || a.Version is AllVersionsSpec || + (a.Version is VersionSpec vs && vs.Min <= versioningSystem.Current) ? 1 : 0) + .ThenByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) + .ThenBy(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) .ToList(); 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); + var heading = CreateApplicabilityHeading(applicability, applicabilityDefinition); + var tooltipText = BuildTooltipText(applicability, applicabilityDefinition, versioningSystem, lifecycleFull); // language=html tooltipParts.Add($"
{heading}{tooltipText}
"); } @@ -98,11 +132,10 @@ private static string BuildCombinedTooltipText( return string.Join("\n\n", tooltipParts); } - private static string CreateApplicabilityHeading(Applicability applicability, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, - SemVersion? realVersion) + private static string CreateApplicabilityHeading(Applicability applicability, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition) { var lifecycleName = applicability.GetLifeCycleName(); - var versionText = realVersion is not null ? $" {realVersion}" : ""; + var versionText = applicability.Version is not null ? $" {applicability.Version.Min}" : ""; // language=html return $"""{applicabilityDefinition.DisplayName} {lifecycleName}{versionText}:"""; } @@ -122,14 +155,13 @@ private static string BuildTooltipText( Applicability applicability, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, VersioningSystem versioningSystem, - SemVersion? realVersion, string lifecycleFull) { var tooltipText = ""; - tooltipText = realVersion is not null - ? realVersion <= versioningSystem.Current - ? $"{lifecycleFull} on {applicabilityDefinition.DisplayName} version {realVersion} and later unless otherwise specified." + tooltipText = applicability.Version is not null && applicability.Version != AllVersionsSpec.Instance + ? applicability.Version.Min <= versioningSystem.Current + ? $"{lifecycleFull} on {applicabilityDefinition.DisplayName} version {applicability.Version.Min} and later unless otherwise specified." : applicability.Lifecycle switch { ProductLifecycle.GenerallyAvailable @@ -167,40 +199,97 @@ or ProductLifecycle.TechnicalPreview private static string BuildBadgeLifecycleText( Applicability applicability, VersioningSystem versioningSystem, - SemVersion? realVersion, AppliesCollection allApplications) { var badgeText = ""; - if (realVersion is not null && realVersion > versioningSystem.Current) + var versionSpec = applicability.Version; + + if (versionSpec is not null && versionSpec != AllVersionsSpec.Instance) { - badgeText = applicability.Lifecycle switch + var isMinReleased = versionSpec.Min <= versioningSystem.Current; + var isMaxReleased = versionSpec.Max is not null && versionSpec.Max <= versioningSystem.Current; + + // Determine if we should show "Planned" badge + var shouldShowPlanned = (versionSpec.Kind == VersionSpecKind.GreaterThanOrEqual && !isMinReleased) + || (versionSpec.Kind == VersionSpecKind.Range && !isMaxReleased && !isMinReleased) + || (versionSpec.Kind == VersionSpecKind.Exact && !isMinReleased); + + // Check lifecycle count for "use previous lifecycle" logic + if (shouldShowPlanned) { - ProductLifecycle.TechnicalPreview => "Planned", - ProductLifecycle.Beta => "Planned", - ProductLifecycle.GenerallyAvailable => - allApplications.Any(a => a.Lifecycle is ProductLifecycle.TechnicalPreview or ProductLifecycle.Beta) - ? "GA planned" - : "Planned", - ProductLifecycle.Deprecated => "Deprecation planned", - ProductLifecycle.Removed => "Removal planned", - ProductLifecycle.Planned => "Planned", - ProductLifecycle.Unavailable => "Unavailable", - _ => badgeText - }; + var lifecycleCount = allApplications.Count; + + // If lifecycle count >= 2, we should use previous lifecycle instead of showing "Planned" + if (lifecycleCount >= 2) + return string.Empty; + + // Otherwise show planned badge (lifecycle count == 1) + badgeText = applicability.Lifecycle switch + { + ProductLifecycle.TechnicalPreview => "Planned", + ProductLifecycle.Beta => "Planned", + ProductLifecycle.GenerallyAvailable => "Planned", + ProductLifecycle.Deprecated => "Deprecation planned", + ProductLifecycle.Removed => "Removal planned", + ProductLifecycle.Planned => "Planned", + ProductLifecycle.Unavailable => "Unavailable", + _ => badgeText + }; + } } return badgeText; } - private static bool TryGetRealVersion(Applicability applicability, [NotNullWhen(true)] out SemVersion? version) + /// + /// Gets the version to display in badges, handling VersionSpec kinds + /// + private static string GetBadgeVersionText(VersionSpec? versionSpec, VersioningSystem versioningSystem) { - version = null; - if (applicability.Version is not null && applicability.Version != AllVersions.Instance) + if (versionSpec is null || versionSpec == AllVersionsSpec.Instance) + return string.Empty; + + var kind = versionSpec.Kind; + var min = versionSpec.Min; + var max = versionSpec.Max; + + // Check if versions are released + var minReleased = min <= versioningSystem.Current; + var maxReleased = max is not null && max <= versioningSystem.Current; + + return kind switch { - version = applicability.Version; - return true; - } + VersionSpecKind.GreaterThanOrEqual => minReleased + ? $"{min.Major}.{min.Minor}+" + : string.Empty, + + VersionSpecKind.Range => maxReleased + ? $"{min.Major}.{min.Minor}-{max!.Major}.{max.Minor}" + : minReleased + ? $"{min.Major}.{min.Minor}+" + : string.Empty, - return false; + VersionSpecKind.Exact => minReleased + ? $"{min.Major}.{min.Minor}" + : string.Empty, + + _ => string.Empty + }; } + private static int GetLifecycleOrder(ProductLifecycle lifecycle) => lifecycle switch + { + ProductLifecycle.GenerallyAvailable => 0, + ProductLifecycle.Beta => 1, + ProductLifecycle.TechnicalPreview => 2, + ProductLifecycle.Planned => 3, + ProductLifecycle.Deprecated => 4, + ProductLifecycle.Removed => 5, + ProductLifecycle.Unavailable => 6, + _ => 999 + }; + + /// + /// Checks if a version should be considered released + /// + private static bool IsVersionReleased(SemVersion version, VersioningSystem versioningSystem) => version <= versioningSystem.Current; } diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml b/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml index a9831405c..815050a7a 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml @@ -12,7 +12,7 @@ @if (item.RenderData.ShowLifecycleName) { - @item.PrimaryApplicability.GetLifeCycleName() + @item.RenderData.LifecycleName } @if (item.RenderData.ShowVersion) { diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs index 3b22299f8..1bf241171 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs @@ -36,8 +36,8 @@ public void RoundTripStackWithVersion() { Stack = new AppliesCollection( [ - new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = new SemVersion(8, 0, 0) }, - new Applicability { Lifecycle = ProductLifecycle.Beta, Version = new SemVersion(7, 17, 0) } + new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }, + new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"7.17.0" } ]) }; @@ -57,8 +57,8 @@ public void RoundTripDeploymentAllProperties() Deployment = new DeploymentApplicability { Self = AppliesCollection.GenerallyAvailable, - Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"3.0.0" }]), - Eck = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"2.0.0" }]), + Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"3.0.0" }]), + Eck = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"2.0.0" }]), Ess = AppliesCollection.GenerallyAvailable } }; @@ -82,8 +82,8 @@ public void RoundTripServerlessAllProperties() Serverless = new ServerlessProjectApplicability { Elasticsearch = AppliesCollection.GenerallyAvailable, - Observability = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = AllVersions.Instance }]), - Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"1.0.0" }]) + Observability = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = AllVersionsSpec.Instance }]), + Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"1.0.0" }]) } }; @@ -140,9 +140,9 @@ public void RoundTripProductApplicabilityMultipleProducts() ProductApplicability = new ProductApplicability { Ecctl = AppliesCollection.GenerallyAvailable, - Curator = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"5.0.0" }]), - ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.2.0" }]), - EdotDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.9.0" }]) + Curator = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"5.0.0" }]), + ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.2.0" }]), + EdotDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.9.0" }]) } }; @@ -165,27 +165,27 @@ public void RoundTripAllProductApplicabilityProperties() ProductApplicability = new ProductApplicability { Ecctl = AppliesCollection.GenerallyAvailable, - Curator = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"5.0.0" }]), - ApmAgentAndroid = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"1.0.0" }]), - ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.2.0" }]), - ApmAgentGo = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"2.0.0" }]), - ApmAgentIos = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"0.5.0" }]), - ApmAgentJava = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.30.0" }]), - ApmAgentNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"3.0.0" }]), - ApmAgentPhp = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.8.0" }]), - ApmAgentPython = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"6.0.0" }]), - ApmAgentRuby = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"4.0.0" }]), - ApmAgentRumJs = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"5.0.0" }]), - EdotIos = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.9.0" }]), - EdotAndroid = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.8.0" }]), - EdotDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.9.0" }]), - EdotJava = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.7.0" }]), - EdotNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.6.0" }]), - EdotPhp = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.5.0" }]), - EdotPython = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.4.0" }]), - EdotCfAws = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"0.3.0" }]), - EdotCfAzure = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"0.2.0" }]), - EdotCollector = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.0.0" }]) + Curator = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"5.0.0" }]), + ApmAgentAndroid = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"1.0.0" }]), + ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.2.0" }]), + ApmAgentGo = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"2.0.0" }]), + ApmAgentIos = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (VersionSpec)"0.5.0" }]), + ApmAgentJava = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.30.0" }]), + ApmAgentNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"3.0.0" }]), + ApmAgentPhp = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.8.0" }]), + ApmAgentPython = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"6.0.0" }]), + ApmAgentRuby = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"4.0.0" }]), + ApmAgentRumJs = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"5.0.0" }]), + EdotIos = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.9.0" }]), + EdotAndroid = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.8.0" }]), + EdotDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.9.0" }]), + EdotJava = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.7.0" }]), + EdotNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.6.0" }]), + EdotPhp = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.5.0" }]), + EdotPython = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.4.0" }]), + EdotCfAws = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (VersionSpec)"0.3.0" }]), + EdotCfAzure = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (VersionSpec)"0.2.0" }]), + EdotCollector = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.0.0" }]) } }; @@ -225,27 +225,27 @@ public void RoundTripComplexAllFieldsPopulated() { Stack = new AppliesCollection( [ - new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" }, - new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"7.17.0" } + new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }, + new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"7.17.0" } ]), Deployment = new DeploymentApplicability { Self = AppliesCollection.GenerallyAvailable, - Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"3.0.0" }]), - Eck = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"2.0.0" }]), + Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"3.0.0" }]), + Eck = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"2.0.0" }]), Ess = AppliesCollection.GenerallyAvailable }, Serverless = new ServerlessProjectApplicability { Elasticsearch = AppliesCollection.GenerallyAvailable, - Observability = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = AllVersions.Instance }]), - Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"1.0.0" }]) + Observability = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = AllVersionsSpec.Instance }]), + Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"1.0.0" }]) }, Product = AppliesCollection.GenerallyAvailable, ProductApplicability = new ProductApplicability { Ecctl = AppliesCollection.GenerallyAvailable, - ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.2.0" }]) + ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.2.0" }]) } }; @@ -274,7 +274,7 @@ public void RoundTripAllLifecycles() { var lifecycles = Enum.GetValues(); var applicabilities = lifecycles.Select(lc => - new Applicability { Lifecycle = lc, Version = (SemVersion)"1.0.0" } + new Applicability { Lifecycle = lc, Version = (VersionSpec)"1.0.0" } ).ToArray(); var original = new ApplicableTo @@ -297,10 +297,10 @@ public void RoundTripMultipleApplicabilitiesInCollection() { Stack = new AppliesCollection( [ - new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" }, - new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"7.17.0" }, - new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"7.16.0" }, - new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"6.0.0" } + new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }, + new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"7.17.0" }, + new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (VersionSpec)"7.16.0" }, + new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"6.0.0" } ]) }; @@ -345,16 +345,16 @@ public void RoundTripAllVersionsSerializesAsSemanticVersion() { var original = new ApplicableTo { - Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = AllVersions.Instance }]) + Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = AllVersionsSpec.Instance }]) }; var json = JsonSerializer.Serialize(original, _options); - json.Should().Contain("\"version\": \"9999.9999.9999\""); + json.Should().Contain("\"version\": \"all\""); var deserialized = JsonSerializer.Deserialize(json, _options); deserialized.Should().NotBeNull(); deserialized!.Stack.Should().NotBeNull(); - deserialized.Stack!.First().Version.Should().Be(AllVersions.Instance); + deserialized.Stack!.First().Version.Should().Be(AllVersionsSpec.Instance); } [Fact] @@ -365,7 +365,7 @@ public void RoundTripProductAndProductApplicabilityBothPresent() Product = AppliesCollection.GenerallyAvailable, ProductApplicability = new ProductApplicability { - Ecctl = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"1.0.0" }]) + Ecctl = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"1.0.0" }]) } }; diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs index fbff52703..e42e47f04 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs @@ -34,7 +34,7 @@ public void SerializeStackProducesCorrectJson() "type": "stack", "sub_type": "stack", "lifecycle": "ga", - "version": "9999.9999.9999" + "version": "all" } ] """); @@ -49,7 +49,7 @@ public void SerializeStackWithVersionProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.Beta, - Version = (SemVersion)"8.0.0" + Version = (VersionSpec)"8.0.0" } ]) }; @@ -64,7 +64,7 @@ public void SerializeStackWithVersionProducesCorrectJson() "type": "stack", "sub_type": "stack", "lifecycle": "beta", - "version": "8.0.0" + "version": "8.0+" } ] """); @@ -80,12 +80,12 @@ public void SerializeMultipleApplicabilitiesProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, - Version = (SemVersion)"8.0.0" + Version = (VersionSpec)"8.0.0" }, new Applicability { Lifecycle = ProductLifecycle.Beta, - Version = (SemVersion)"7.17.0" + Version = (VersionSpec)"7.17.0" } ]) }; @@ -100,13 +100,13 @@ public void SerializeMultipleApplicabilitiesProducesCorrectJson() "type": "stack", "sub_type": "stack", "lifecycle": "ga", - "version": "8.0.0" + "version": "8.0+" }, { "type": "stack", "sub_type": "stack", "lifecycle": "beta", - "version": "7.17.0" + "version": "7.17+" } ] """); @@ -123,7 +123,7 @@ public void SerializeDeploymentProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, - Version = (SemVersion)"3.0.0" + Version = (VersionSpec)"3.0.0" } ]), Ess = AppliesCollection.GenerallyAvailable @@ -140,13 +140,13 @@ public void SerializeDeploymentProducesCorrectJson() "type": "deployment", "sub_type": "ece", "lifecycle": "ga", - "version": "3.0.0" + "version": "3.0+" }, { "type": "deployment", "sub_type": "ess", "lifecycle": "ga", - "version": "9999.9999.9999" + "version": "all" } ] """); @@ -163,7 +163,7 @@ public void SerializeServerlessProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.Beta, - Version = (SemVersion)"1.0.0" + Version = (VersionSpec)"1.0.0" } ]), Security = AppliesCollection.GenerallyAvailable @@ -180,13 +180,13 @@ public void SerializeServerlessProducesCorrectJson() "type": "serverless", "sub_type": "elasticsearch", "lifecycle": "beta", - "version": "1.0.0" + "version": "1.0+" }, { "type": "serverless", "sub_type": "security", "lifecycle": "ga", - "version": "9999.9999.9999" + "version": "all" } ] """); @@ -201,7 +201,7 @@ public void SerializeProductProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, - Version = (SemVersion)"0.5.0" + Version = (VersionSpec)"0.5.0" } ]) }; @@ -216,7 +216,7 @@ public void SerializeProductProducesCorrectJson() "type": "product", "sub_type": "product", "lifecycle": "preview", - "version": "0.5.0" + "version": "0.5+" } ] """); @@ -233,7 +233,7 @@ public void SerializeProductApplicabilityProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.Deprecated, - Version = (SemVersion)"5.0.0" + Version = (VersionSpec)"5.0.0" } ]), ApmAgentDotnet = AppliesCollection.GenerallyAvailable @@ -250,13 +250,13 @@ public void SerializeProductApplicabilityProducesCorrectJson() "type": "product", "sub_type": "ecctl", "lifecycle": "deprecated", - "version": "5.0.0" + "version": "5.0+" }, { "type": "product", "sub_type": "apm-agent-dotnet", "lifecycle": "ga", - "version": "9999.9999.9999" + "version": "all" } ] """); @@ -272,27 +272,27 @@ public void SerializeAllLifecyclesProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, - Version = (SemVersion)"1.0.0" + Version = (VersionSpec)"1.0.0" }, new Applicability { Lifecycle = ProductLifecycle.Beta, - Version = (SemVersion)"1.0.0" + Version = (VersionSpec)"1.0.0" }, new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, - Version = (SemVersion)"1.0.0" + Version = (VersionSpec)"1.0.0" }, new Applicability { Lifecycle = ProductLifecycle.Deprecated, - Version = (SemVersion)"1.0.0" + Version = (VersionSpec)"1.0.0" }, new Applicability { Lifecycle = ProductLifecycle.Removed, - Version = (SemVersion)"1.0.0" + Version = (VersionSpec)"1.0.0" } ]) }; @@ -315,7 +315,7 @@ public void SerializeComplexProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, - Version = (SemVersion)"8.0.0" + Version = (VersionSpec)"8.0.0" } ]), Deployment = new DeploymentApplicability @@ -364,7 +364,7 @@ public void SerializeValidatesJsonStructure() new Applicability { Lifecycle = ProductLifecycle.Beta, - Version = (SemVersion)"3.0.0" + Version = (VersionSpec)"3.0.0" } ]) } @@ -383,12 +383,12 @@ public void SerializeValidatesJsonStructure() stackEntry.GetProperty("type").GetString().Should().Be("stack"); stackEntry.GetProperty("sub_type").GetString().Should().Be("stack"); stackEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - stackEntry.GetProperty("version").GetString().Should().Be("9999.9999.9999"); + stackEntry.GetProperty("version").GetString().Should().Be("all"); var deploymentEntry = array[1]; deploymentEntry.GetProperty("type").GetString().Should().Be("deployment"); deploymentEntry.GetProperty("sub_type").GetString().Should().Be("ece"); deploymentEntry.GetProperty("lifecycle").GetString().Should().Be("beta"); - deploymentEntry.GetProperty("version").GetString().Should().Be("3.0.0"); + deploymentEntry.GetProperty("version").GetString().Should().Be("3.0+"); } } diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs index 3cefe5c4a..6e76d6f84 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs @@ -50,7 +50,7 @@ public void ProductApplicabilityToStringWithSomePropertiesOnlyIncludesSetPropert var productApplicability = new ProductApplicability { ApmAgentDotnet = AppliesCollection.GenerallyAvailable, - Ecctl = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = new SemVersion(1, 0, 0) }]) + Ecctl = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = VersionSpec.TryParse("1.0.0", out var v) ? v : null }]) }; var result = productApplicability.ToString(); diff --git a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs index f0f372d2d..b60ea8d17 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs @@ -227,11 +227,12 @@ public void GeneratesDeterministicSyncKeysAcrossMultipleRuns() var expectedKeys = new Dictionary { // These are the actual SHA256-based hashes that should never change - { "stack: ga 9.1", "applies-031B7112" }, - { "stack: preview 9.0", "applies-361F73DC" }, - { "ess: ga 8.11", "applies-32E204F7" }, - { "deployment: { ece: ga 9.0, ess: ga 9.1 }", "applies-D099CDEF" }, - { "serverless: all", "applies-A34B17C6" }, + // (unless the version format actually changes) + { "stack: ga 9.1", "applies-A8B9CC9C" }, + { "stack: preview 9.0", "applies-66AECC4E" }, + { "ess: ga 8.11", "applies-9CA8543E" }, + { "deployment: { ece: ga 9.0, ess: ga 9.1 }", "applies-51C670D4" }, + { "serverless: all", "applies-A34B17C6" } }; foreach (var (definition, expectedKey) in expectedKeys) diff --git a/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs index e2090cd0c..def825530 100644 --- a/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs +++ b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs @@ -47,7 +47,7 @@ public void SerializeDocumentWithStackAppliesToProducesCorrectJson() stackEntry.GetProperty("type").GetString().Should().Be("stack"); stackEntry.GetProperty("sub_type").GetString().Should().Be("stack"); stackEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - stackEntry.GetProperty("version").GetString().Should().Be("9999.9999.9999"); + stackEntry.GetProperty("version").GetString().Should().Be("all"); } [Fact] @@ -64,7 +64,7 @@ public void SerializeDocumentWithDeploymentAppliesToProducesCorrectJson() Deployment = new DeploymentApplicability { Ess = AppliesCollection.GenerallyAvailable, - Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"3.5.0" }]) + Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"3.5.0" }]) } } }; @@ -82,14 +82,14 @@ public void SerializeDocumentWithDeploymentAppliesToProducesCorrectJson() essEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); essEntry.GetProperty("type").GetString().Should().Be("deployment"); essEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - essEntry.GetProperty("version").GetString().Should().Be("9999.9999.9999"); + essEntry.GetProperty("version").GetString().Should().Be("all"); // Verify ECE entry var eceEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "ece"); eceEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); eceEntry.GetProperty("type").GetString().Should().Be("deployment"); eceEntry.GetProperty("lifecycle").GetString().Should().Be("beta"); - eceEntry.GetProperty("version").GetString().Should().Be("3.5.0"); + eceEntry.GetProperty("version").GetString().Should().Be("3.5+"); } [Fact] @@ -105,8 +105,8 @@ public void SerializeDocumentWithServerlessAppliesToProducesCorrectJson() { Serverless = new ServerlessProjectApplicability { - Elasticsearch = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" }]), - Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"1.0.0" }]) + Elasticsearch = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }]), + Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (VersionSpec)"1.0.0" }]) } } }; @@ -124,14 +124,14 @@ public void SerializeDocumentWithServerlessAppliesToProducesCorrectJson() esEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); esEntry.GetProperty("type").GetString().Should().Be("serverless"); esEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - esEntry.GetProperty("version").GetString().Should().Be("8.0.0"); + esEntry.GetProperty("version").GetString().Should().Be("8.0+"); // Verify security entry var secEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "security"); secEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); secEntry.GetProperty("type").GetString().Should().Be("serverless"); secEntry.GetProperty("lifecycle").GetString().Should().Be("preview"); - secEntry.GetProperty("version").GetString().Should().Be("1.0.0"); + secEntry.GetProperty("version").GetString().Should().Be("1.0+"); } [Fact] @@ -145,7 +145,7 @@ public void SerializeDocumentWithProductAppliesToProducesCorrectJson() SearchTitle = "Product Test", Applies = new ApplicableTo { - Product = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"2.0.0" }]) + Product = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"2.0.0" }]) } }; @@ -161,7 +161,7 @@ public void SerializeDocumentWithProductAppliesToProducesCorrectJson() productEntry.GetProperty("type").GetString().Should().Be("product"); productEntry.GetProperty("sub_type").GetString().Should().Be("product"); productEntry.GetProperty("lifecycle").GetString().Should().Be("beta"); - productEntry.GetProperty("version").GetString().Should().Be("2.0.0"); + productEntry.GetProperty("version").GetString().Should().Be("2.0+"); } [Fact] @@ -177,8 +177,8 @@ public void SerializeDocumentWithProductApplicabilityProducesCorrectJson() { ProductApplicability = new ProductApplicability { - ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.5.0" }]), - ApmAgentNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"2.0.0" }]) + ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.5.0" }]), + ApmAgentNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"2.0.0" }]) } } }; @@ -196,14 +196,14 @@ public void SerializeDocumentWithProductApplicabilityProducesCorrectJson() dotnetEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); dotnetEntry.GetProperty("type").GetString().Should().Be("product"); dotnetEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - dotnetEntry.GetProperty("version").GetString().Should().Be("1.5.0"); + dotnetEntry.GetProperty("version").GetString().Should().Be("1.5+"); // Verify apm-agent-node entry var nodeEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "apm-agent-node"); nodeEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); nodeEntry.GetProperty("type").GetString().Should().Be("product"); nodeEntry.GetProperty("lifecycle").GetString().Should().Be("deprecated"); - nodeEntry.GetProperty("version").GetString().Should().Be("2.0.0"); + nodeEntry.GetProperty("version").GetString().Should().Be("2.0+"); } [Fact] @@ -217,7 +217,7 @@ public void SerializeDocumentWithComplexAppliesToProducesCorrectJson() SearchTitle = "Complex Test", Applies = new ApplicableTo { - Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" }]), + Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }]), Deployment = new DeploymentApplicability { Ess = AppliesCollection.GenerallyAvailable @@ -305,10 +305,10 @@ public void RoundTripDocumentWithAppliesToPreservesData() LastUpdated = DateTimeOffset.Parse("2024-01-15T09:00:00Z", CultureInfo.InvariantCulture), Applies = new ApplicableTo { - Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.5.0" }]), + Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.5.0" }]), Deployment = new DeploymentApplicability { - Ess = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"8.6.0" }]) + Ess = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"8.6.0" }]) } }, Headings = ["Introduction", "Getting Started"], @@ -343,9 +343,9 @@ public void SerializeDocumentWithMultipleApplicabilitiesPerTypeProducesMultipleA { Stack = new AppliesCollection( [ - new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" }, - new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"7.17.0" }, - new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"7.0.0" } + new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }, + new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"7.17.0" }, + new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"7.0.0" } ]) } }; diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index 2878c4a95..bbc93c67f 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -413,8 +413,8 @@ If this functionality is unavailable or behaves differently when deployed on ECH 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."> Stack - - GA planned + + Preview @@ -526,7 +526,7 @@ This functionality may be changed or removed in a future release. Elastic will w Preview - 1.3.0 + 1.3+ @@ -824,7 +824,7 @@ Beta features are subject to change. The design and code is less mature than off GA - 8.0.0 + 8.0+ diff --git a/tests/authoring/Inline/AppliesToRole.fs b/tests/authoring/Inline/AppliesToRole.fs index 6583a0d79..a4f60ba3d 100644 --- a/tests/authoring/Inline/AppliesToRole.fs +++ b/tests/authoring/Inline/AppliesToRole.fs @@ -150,8 +150,8 @@ If this functionality is unavailable or behaves differently when deployed on ECH 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."> Stack - - GA planned + + Preview From e6d710f987fd0fd55587c66151185d2c318b510d Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 8 Dec 2025 04:04:27 -0300 Subject: [PATCH 03/61] Show base version if we don't get a specified version for a versioned product --- .../AppliesTo/ApplicableToYamlConverter.cs | 6 ++-- src/Elastic.Documentation/SemVersion.cs | 2 +- .../Myst/Components/ApplicabilityRenderer.cs | 29 ++++++++++++++++--- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs index 2b6269034..fc6da5ed2 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs @@ -284,7 +284,7 @@ private static void ValidateApplicabilityCollection(string key, AppliesCollectio // Rule: Only one item per key can use greater-than syntax var greaterThanItems = items.Where(a => - a.Version is VersionSpec spec && spec.Kind == VersionSpecKind.GreaterThanOrEqual && + a.Version is { Kind: VersionSpecKind.GreaterThanOrEqual } && a.Version != AllVersionsSpec.Instance).ToList(); if (greaterThanItems.Count > 1) @@ -335,8 +335,8 @@ private static bool CheckVersionOverlap(VersionSpec v1, VersionSpec v2, out stri var (v1Min, v1Max) = GetEffectiveRange(v1); var (v2Min, v2Max) = GetEffectiveRange(v2); - var overlaps = v1Min.CompareTo(v2Max ?? new SemVersion(9999, 9999, 9999)) <= 0 && - v2Min.CompareTo(v1Max ?? new SemVersion(9999, 9999, 9999)) <= 0; + var overlaps = v1Min.CompareTo(v2Max ?? new SemVersion(99999, 99999, 99999)) <= 0 && + v2Min.CompareTo(v1Max ?? new SemVersion(99999, 99999, 99999)) <= 0; if (overlaps) message = $"Version ranges overlap."; diff --git a/src/Elastic.Documentation/SemVersion.cs b/src/Elastic.Documentation/SemVersion.cs index 0516f22e0..8ee4f2cec 100644 --- a/src/Elastic.Documentation/SemVersion.cs +++ b/src/Elastic.Documentation/SemVersion.cs @@ -8,7 +8,7 @@ namespace Elastic.Documentation; -public class AllVersions() : SemVersion(9999, 9999, 9999) +public class AllVersions() : SemVersion(99999, 99999, 99999) { public static AllVersions Instance { get; } = new(); } diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 7984fe4a8..73fba4a51 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -2,7 +2,6 @@ // 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.Diagnostics.CodeAnalysis; using Elastic.Documentation; using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Configuration.Versions; @@ -159,8 +158,10 @@ private static string BuildTooltipText( { var tooltipText = ""; - tooltipText = applicability.Version is not null && applicability.Version != AllVersionsSpec.Instance - ? applicability.Version.Min <= versioningSystem.Current + // Check if a specific version is provided + if (applicability.Version is not null && applicability.Version != AllVersionsSpec.Instance) + { + tooltipText = applicability.Version.Min <= versioningSystem.Current ? $"{lifecycleFull} on {applicabilityDefinition.DisplayName} version {applicability.Version.Min} and later unless otherwise specified." : applicability.Lifecycle switch { @@ -174,8 +175,21 @@ or ProductLifecycle.TechnicalPreview ProductLifecycle.Removed => $"We plan to remove this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.", _ => tooltipText + }; + } + else + { + // No version specified - check if we should show base version + tooltipText = versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major + ? applicability.Lifecycle switch + { + ProductLifecycle.Removed => + $"Removed in {applicabilityDefinition.DisplayName} {versioningSystem.Base.Major}.{versioningSystem.Base.Minor}.", + _ => + $"{lifecycleFull} since {versioningSystem.Base.Major}.{versioningSystem.Base.Minor}." } - : $"{lifecycleFull} on {applicabilityDefinition.DisplayName} unless otherwise specified."; + : $"{lifecycleFull} on {applicabilityDefinition.DisplayName} unless otherwise specified."; + } var disclaimer = GetDisclaimer(applicability.Lifecycle, versioningSystem.Id); if (disclaimer is not null) @@ -246,8 +260,15 @@ private static string BuildBadgeLifecycleText( ///
private static string GetBadgeVersionText(VersionSpec? versionSpec, VersioningSystem versioningSystem) { + // When no version is specified, check if we should show the base version if (versionSpec is null || versionSpec == AllVersionsSpec.Instance) + { + if (versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major) + return $"{versioningSystem.Base.Major}.{versioningSystem.Base.Minor}+"; + + // Otherwise, this is an unversioned product, show no version return string.Empty; + } var kind = versionSpec.Kind; var min = versionSpec.Min; From 4bd996a2b3123ee7021841c0d048fdd0dec1eb2a Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 8 Dec 2025 04:37:03 -0300 Subject: [PATCH 04/61] Adjust tests to better match the currently expected output --- .../Myst/Components/ApplicabilityRenderer.cs | 4 ++-- .../ApplicableToJsonConverterSerializationTests.cs | 4 +++- tests/authoring/Applicability/ApplicableToComponent.fs | 8 ++++---- tests/authoring/Blocks/Admonitions.fs | 6 +++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 73fba4a51..6895236fb 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -114,7 +114,7 @@ private static string BuildCombinedTooltipText( // 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 AllVersionsSpec || - (a.Version is VersionSpec vs && vs.Min <= versioningSystem.Current) ? 1 : 0) + (a.Version is { } vs && vs.Min <= versioningSystem.Current) ? 1 : 0) .ThenByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) .ThenBy(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) .ToList(); @@ -146,7 +146,7 @@ private static string CreateApplicabilityHeading(Applicability applicability, Ap ProductLifecycle.TechnicalPreview => "Available in technical preview", ProductLifecycle.Deprecated => "Deprecated", ProductLifecycle.Removed => "Removed", - ProductLifecycle.Unavailable => "Not available", + ProductLifecycle.Unavailable => "Unavailable", _ => "" }; diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs index e42e47f04..4e47b5edd 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs @@ -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 System.Text.Encodings.Web; using System.Text.Json; using Elastic.Documentation; using Elastic.Documentation.AppliesTo; @@ -13,7 +14,8 @@ public class ApplicableToJsonConverterSerializationTests { private readonly JsonSerializerOptions _options = new() { - WriteIndented = true + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; [Fact] diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index bbc93c67f..af138871e 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -142,7 +142,7 @@ stack: ga let ``renders all versions`` () = markdown |> convertsToHtml """

- Stack @@ -479,7 +479,7 @@ stack: unavailable let ``renders unavailable`` () = markdown |> convertsToHtml """

- + Stack @@ -500,7 +500,7 @@ product: ga let ``renders product all versions`` () = markdown |> convertsToHtml """

- + @@ -694,7 +694,7 @@ stack: let ``renders missing edge cases`` () = markdown |> convertsToHtml """

- Stack diff --git a/tests/authoring/Blocks/Admonitions.fs b/tests/authoring/Blocks/Admonitions.fs index d7efdb64b..324f3c5b4 100644 --- a/tests/authoring/Blocks/Admonitions.fs +++ b/tests/authoring/Blocks/Admonitions.fs @@ -64,7 +64,7 @@ This is a custom admonition with applies_to information.

Note - Stack @@ -82,7 +82,7 @@ If this functionality is unavailable or behaves differently when deployed on ECH
Warning - + Serverless @@ -98,7 +98,7 @@ If this functionality is unavailable or behaves differently when deployed on ECH
Tip - Serverless Elasticsearch From 7ead71683bc5f84d54c7e5687e7eaba37872bcbf Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 8 Dec 2025 05:04:35 -0300 Subject: [PATCH 05/61] Add VersionSpec to YamlSerialization --- src/Elastic.Markdown/Myst/YamlSerialization.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Elastic.Markdown/Myst/YamlSerialization.cs b/src/Elastic.Markdown/Myst/YamlSerialization.cs index 4bf08dad3..0728b39b8 100644 --- a/src/Elastic.Markdown/Myst/YamlSerialization.cs +++ b/src/Elastic.Markdown/Myst/YamlSerialization.cs @@ -21,6 +21,7 @@ public static T Deserialize(string yaml, ProductsConfiguration products) .IgnoreUnmatchedProperties() .WithEnumNamingConvention(HyphenatedNamingConvention.Instance) .WithTypeConverter(new SemVersionConverter()) + .WithTypeConverter(new VersionSpecConverter()) .WithTypeConverter(new ProductConverter(products)) .WithTypeConverter(new ApplicableToYamlConverter(products.Products.Keys)) .Build(); From fcae6286f82493d0d377c48195c85dec80ab90f0 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 8 Dec 2025 05:09:03 -0300 Subject: [PATCH 06/61] Typo! --- .../AppliesTo/ApplicableToJsonConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs index c36e350dc..336c03d12 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs @@ -72,7 +72,7 @@ public class ApplicableToJsonConverter : JsonConverter break; case "version": var versionStr = reader.GetString(); - if (versionStr != null && VersionSpecConverter.TryParse(versionStr, out var v)) + if (versionStr != null && VersionSpec.TryParse(versionStr, out var v)) version = v; break; } From e435de96c35f4d81bc785ca99838d50e351e203f Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 8 Dec 2025 05:11:50 -0300 Subject: [PATCH 07/61] No need for it to be initialized here after all. --- src/Elastic.Markdown/Myst/YamlSerialization.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Elastic.Markdown/Myst/YamlSerialization.cs b/src/Elastic.Markdown/Myst/YamlSerialization.cs index 0728b39b8..4bf08dad3 100644 --- a/src/Elastic.Markdown/Myst/YamlSerialization.cs +++ b/src/Elastic.Markdown/Myst/YamlSerialization.cs @@ -21,7 +21,6 @@ public static T Deserialize(string yaml, ProductsConfiguration products) .IgnoreUnmatchedProperties() .WithEnumNamingConvention(HyphenatedNamingConvention.Instance) .WithTypeConverter(new SemVersionConverter()) - .WithTypeConverter(new VersionSpecConverter()) .WithTypeConverter(new ProductConverter(products)) .WithTypeConverter(new ApplicableToYamlConverter(products.Products.Keys)) .Build(); From 7f5f2be0f6b2d5cb0784fb1bd76fb86431c04077 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 8 Dec 2025 05:28:31 -0300 Subject: [PATCH 08/61] Handle "all" explicitly --- .../AppliesTo/ApplicableToJsonConverter.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs index 336c03d12..c8d987064 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs @@ -72,8 +72,14 @@ public class ApplicableToJsonConverter : JsonConverter break; case "version": var versionStr = reader.GetString(); - if (versionStr != null && VersionSpec.TryParse(versionStr, out var v)) - version = v; + if (versionStr != null) + { + // Handle "all" explicitly for AllVersionsSpec + if (string.Equals(versionStr.Trim(), "all", StringComparison.OrdinalIgnoreCase)) + version = AllVersionsSpec.Instance; + else if (VersionSpec.TryParse(versionStr, out var v)) + version = v; + } break; } } From 00ab8d0d4d8fb8e8decc14b1ef803972b1977db6 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 8 Dec 2025 05:47:57 -0300 Subject: [PATCH 09/61] Adopting a few review suggestions --- .../AppliesTo/ApplicabilitySelector.cs | 6 ++---- .../AppliesTo/ApplicableToYamlConverter.cs | 12 +++++------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs index d090f2b02..4dcc98494 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs @@ -30,8 +30,7 @@ public static Applicability GetPrimaryApplicability(IEnumerable a }; var availableApplicabilities = applicabilityList - .Where(a => a.Version is null || a.Version is AllVersionsSpec || - (a.Version is VersionSpec vs && vs.Min <= currentVersion)) + .Where(a => a.Version is null || a.Version is AllVersionsSpec || a.Version.Min <= currentVersion) .ToList(); if (availableApplicabilities.Count != 0) @@ -43,8 +42,7 @@ public static Applicability GetPrimaryApplicability(IEnumerable a } var futureApplicabilities = applicabilityList - .Where(a => a.Version is not null && a.Version is not AllVersionsSpec && - a.Version is VersionSpec vs && vs.Min > currentVersion) + .Where(a => a.Version is not null && a.Version is not AllVersionsSpec && a.Version.Min > currentVersion) .ToList(); if (futureApplicabilities.Count != 0) diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs index fc6da5ed2..3db7a6247 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs @@ -294,15 +294,13 @@ private static void ValidateApplicabilityCollection(string key, AppliesCollectio } // Rule: In a range, the first version must be less than or equal the last version - foreach (var item in items) + foreach (var item in items.Where(a => a.Version is { Kind: VersionSpecKind.Range })) { - if (item.Version is { Kind: VersionSpecKind.Range } spec) + var spec = item.Version!; + if (spec.Min.CompareTo(spec.Max!) > 0) { - if (spec.Min.CompareTo(spec.Max!) > 0) - { - diagnostics.Add((Severity.Warning, - $"Key '{key}', {item.Lifecycle}: Range has first version ({spec.Min.Major}.{spec.Min.Minor}) greater than last version ({spec.Max!.Major}.{spec.Max.Minor}).")); - } + diagnostics.Add((Severity.Warning, + $"Key '{key}', {item.Lifecycle}: Range has first version ({spec.Min.Major}.{spec.Min.Minor}) greater than last version ({spec.Max!.Major}.{spec.Max.Minor}).")); } } From 2613a137d4b33f9ee2135be34e4f43714d62154b Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 8 Dec 2025 09:42:47 -0300 Subject: [PATCH 10/61] Fix warnings on docs-builder docs --- docs/syntax/_snippets/inline-level-applies-examples.md | 4 ++-- docs/testing/req.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/syntax/_snippets/inline-level-applies-examples.md b/docs/syntax/_snippets/inline-level-applies-examples.md index 58f38476c..1fedf4759 100644 --- a/docs/syntax/_snippets/inline-level-applies-examples.md +++ b/docs/syntax/_snippets/inline-level-applies-examples.md @@ -55,7 +55,7 @@ This example shows how to use directly a key from the second level of the `appli ::::{tab-item} Output - {applies_to}`serverless: ga` {applies_to}`stack: ga 9.1.0` -- {applies_to}`edot_python: preview 1.7.0, ga 1.8.0` {applies_to}`apm_agent_java: beta 1.0.0, ga 1.2.0` +- {applies_to}`edot_python: preview =1.7.0, ga 1.8.0` {applies_to}`apm_agent_java: beta =1.0.0, ga 1.2.0` - {applies_to}`stack: ga 9.0` {applies_to}`eck: ga 3.0` :::: @@ -63,7 +63,7 @@ This example shows how to use directly a key from the second level of the `appli ::::{tab-item} Markdown ```markdown - {applies_to}`serverless: ga` {applies_to}`stack: ga 9.1.0` -- {applies_to}`edot_python: preview 1.7.0, ga 1.8.0` {applies_to}`apm_agent_java: beta 1.0.0, ga 1.2.0` +- {applies_to}`edot_python: preview =1.7.0, ga 1.8.0` {applies_to}`apm_agent_java: beta =1.0.0, ga 1.2.0` - {applies_to}`stack: ga 9.0` {applies_to}`eck: ga 3.0` ``` :::: diff --git a/docs/testing/req.md b/docs/testing/req.md index 95907215f..8db88e205 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -9,7 +9,7 @@ mapped_pages: # Requirements ```{applies_to} -stack: preview 9.0, ga 9.1 +stack: preview =9.0, ga 9.1 ``` 1. Select **Create** to create a new policy, or select **Edit** {icon}`pencil` to open an existing policy. From d4cce986d167c0004ed5166a3f1b1eb11a0851f5 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 8 Dec 2025 12:58:57 -0300 Subject: [PATCH 11/61] Include applicability table in req.md --- docs/testing/req.md | 74 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index 8db88e205..ef778ed3e 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -20,13 +20,9 @@ stack: preview =9.0, ga 9.1 This tutorial is based on Elasticsearch 9.0. This tutorial is based on Elasticsearch 9.0. This tutorial is based on Elasticsearch 9.0. -what - - To follow this tutorial you will need to install the following components: - - An installation of Elasticsearch, based on our hosted [Elastic Cloud](https://www.elastic.co/cloud) service (which includes a free trial period), or a self-hosted service that you run on your own computer. See the Install Elasticsearch section above for installation instructions. - A [Python](https://python.org) interpreter. Make sure it is a recent version, such as Python 3.8 or newer. @@ -38,3 +34,73 @@ The tutorial assumes that you have no previous knowledge of Elasticsearch or gen {applies_to}`ece: removed` + +## Applies To Badge Scenarios + +Below is a table of `applies_to` badge scenarios. + +### No version specified (serverless) + +| Badge | Raw Markdown | +|-------|--------------| +| {applies_to}`serverless: ga` | ````markdown
{applies_to}`serverless: ga`
```` | +| {applies_to}`serverless: preview` | ````markdown
{applies_to}`serverless: preview`
```` | +| {applies_to}`serverless: beta` | ````markdown
{applies_to}`serverless: beta`
```` | +| {applies_to}`serverless: deprecated` | ````markdown
{applies_to}`serverless: deprecated`
```` | +| {applies_to}`serverless: removed` | ````markdown
{applies_to}`serverless: removed`
```` | + +### No version specified (stack) + +| Badge | Raw Markdown | +|-------|--------------| +| {applies_to}`stack: ga` | ````markdown
{applies_to}`stack: ga`
```` | +| {applies_to}`stack: preview` | ````markdown
{applies_to}`stack: preview`
```` | +| {applies_to}`stack: beta` | ````markdown
{applies_to}`stack: beta`
```` | +| {applies_to}`stack: deprecated` | ````markdown
{applies_to}`stack: deprecated`
```` | +| {applies_to}`stack: removed` | ````markdown
{applies_to}`stack: removed`
```` | + +### Greater than or equal to (x.x+ / x.x) + +| Badge | Raw Markdown | +|-------|--------------| +| {applies_to}`stack: ga 9.1` | ````markdown
{applies_to}`stack: ga 9.1`
```` | +| {applies_to}`stack: ga 9.1+` | ````markdown
{applies_to}`stack: ga 9.1+`
```` | +| {applies_to}`stack: preview 9.0+` | ````markdown
{applies_to}`stack: preview 9.0+`
```` | +| {applies_to}`stack: beta 9.1+` | ````markdown
{applies_to}`stack: beta 9.1+`
```` | +| {applies_to}`stack: deprecated 9.0+` | ````markdown
{applies_to}`stack: deprecated 9.0+`
```` | +| {applies_to}`stack: removed 9.0` | ````markdown
{applies_to}`stack: removed 9.0`
```` | + +### Range (x.x-y.y) + +| Badge | Raw Markdown | +|-------|--------------| +| {applies_to}`stack: ga 9.0-9.2` | ````markdown
{applies_to}`stack: ga 9.0-9.2`
```` | +| {applies_to}`stack: preview 9.0-9.2` | ````markdown
{applies_to}`stack: preview 9.0-9.2`
```` | +| {applies_to}`stack: beta 9.0-9.1` | ````markdown
{applies_to}`stack: beta 9.0-9.1`
```` | +| {applies_to}`stack: deprecated 9.0-9.2` | ````markdown
{applies_to}`stack: deprecated 9.0-9.2`
```` | + +### Exact version (=x.x) + +| Badge | Raw Markdown | +|-------|--------------| +| {applies_to}`stack: ga =9.1` | ````markdown
{applies_to}`stack: ga =9.1`
```` | +| {applies_to}`stack: preview =9.0` | ````markdown
{applies_to}`stack: preview =9.0`
```` | +| {applies_to}`stack: beta =9.1` | ````markdown
{applies_to}`stack: beta =9.1`
```` | +| {applies_to}`stack: deprecated =9.0` | ````markdown
{applies_to}`stack: deprecated =9.0`
```` | +| {applies_to}`stack: removed =9.0` | ````markdown
{applies_to}`stack: removed =9.0`
```` | + +### Multiple lifecycles + +| Badge | Raw Markdown | +|-------|--------------| +| {applies_to}`stack: ga 9.2+, beta 9.0-9.1` | ````markdown
{applies_to}`stack: ga 9.2+, beta 9.0-9.1`
```` | +| {applies_to}`stack: ga 9.2+, preview 9.0-9.1` | ````markdown
{applies_to}`stack: ga 9.2+, preview 9.0-9.1`
```` | + +### Deployment types + +| Badge | Raw Markdown | +|-------|--------------| +| {applies_to}`ece: ga 9.0+` | ````markdown
{applies_to}`ece: ga 9.0+`
```` | +| {applies_to}`eck: preview 9.1+` | ````markdown
{applies_to}`eck: preview 9.1+`
```` | +| {applies_to}`ece: deprecated 6.7+` | ````markdown
{applies_to}`ece: deprecated 6.7+`
```` | +| {applies_to}`ece: removed` | ````markdown
{applies_to}`ece: removed`
```` | From 043c470aff54a16ce0fe144d4853847634c10fcd Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 8 Dec 2025 13:10:48 -0300 Subject: [PATCH 12/61] Fix markdown formatting --- docs/testing/req.md | 62 ++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index ef778ed3e..9ebb6e4ff 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -43,64 +43,64 @@ Below is a table of `applies_to` badge scenarios. | Badge | Raw Markdown | |-------|--------------| -| {applies_to}`serverless: ga` | ````markdown
{applies_to}`serverless: ga`
```` | -| {applies_to}`serverless: preview` | ````markdown
{applies_to}`serverless: preview`
```` | -| {applies_to}`serverless: beta` | ````markdown
{applies_to}`serverless: beta`
```` | -| {applies_to}`serverless: deprecated` | ````markdown
{applies_to}`serverless: deprecated`
```` | -| {applies_to}`serverless: removed` | ````markdown
{applies_to}`serverless: removed`
```` | +| {applies_to}`serverless: ga` | `` {applies_to}`serverless: ga` `` | +| {applies_to}`serverless: preview` | `` {applies_to}`serverless: preview` `` | +| {applies_to}`serverless: beta` | `` {applies_to}`serverless: beta` `` | +| {applies_to}`serverless: deprecated` | `` {applies_to}`serverless: deprecated` `` | +| {applies_to}`serverless: removed` | `` {applies_to}`serverless: removed` `` | ### No version specified (stack) | Badge | Raw Markdown | |-------|--------------| -| {applies_to}`stack: ga` | ````markdown
{applies_to}`stack: ga`
```` | -| {applies_to}`stack: preview` | ````markdown
{applies_to}`stack: preview`
```` | -| {applies_to}`stack: beta` | ````markdown
{applies_to}`stack: beta`
```` | -| {applies_to}`stack: deprecated` | ````markdown
{applies_to}`stack: deprecated`
```` | -| {applies_to}`stack: removed` | ````markdown
{applies_to}`stack: removed`
```` | +| {applies_to}`stack: ga` | `` {applies_to}`stack: ga` `` | +| {applies_to}`stack: preview` | `` {applies_to}`stack: preview` `` | +| {applies_to}`stack: beta` | `` {applies_to}`stack: beta` `` | +| {applies_to}`stack: deprecated` | `` {applies_to}`stack: deprecated` `` | +| {applies_to}`stack: removed` | `` {applies_to}`stack: removed` `` | ### Greater than or equal to (x.x+ / x.x) | Badge | Raw Markdown | |-------|--------------| -| {applies_to}`stack: ga 9.1` | ````markdown
{applies_to}`stack: ga 9.1`
```` | -| {applies_to}`stack: ga 9.1+` | ````markdown
{applies_to}`stack: ga 9.1+`
```` | -| {applies_to}`stack: preview 9.0+` | ````markdown
{applies_to}`stack: preview 9.0+`
```` | -| {applies_to}`stack: beta 9.1+` | ````markdown
{applies_to}`stack: beta 9.1+`
```` | -| {applies_to}`stack: deprecated 9.0+` | ````markdown
{applies_to}`stack: deprecated 9.0+`
```` | -| {applies_to}`stack: removed 9.0` | ````markdown
{applies_to}`stack: removed 9.0`
```` | +| {applies_to}`stack: ga 9.1` | `` {applies_to}`stack: ga 9.1` `` | +| {applies_to}`stack: ga 9.1+` | `` {applies_to}`stack: ga 9.1+` `` | +| {applies_to}`stack: preview 9.0+` | `` {applies_to}`stack: preview 9.0+` `` | +| {applies_to}`stack: beta 9.1+` | `` {applies_to}`stack: beta 9.1+` `` | +| {applies_to}`stack: deprecated 9.0+` | `` {applies_to}`stack: deprecated 9.0+` `` | +| {applies_to}`stack: removed 9.0` | `` {applies_to}`stack: removed 9.0` `` | ### Range (x.x-y.y) | Badge | Raw Markdown | |-------|--------------| -| {applies_to}`stack: ga 9.0-9.2` | ````markdown
{applies_to}`stack: ga 9.0-9.2`
```` | -| {applies_to}`stack: preview 9.0-9.2` | ````markdown
{applies_to}`stack: preview 9.0-9.2`
```` | -| {applies_to}`stack: beta 9.0-9.1` | ````markdown
{applies_to}`stack: beta 9.0-9.1`
```` | -| {applies_to}`stack: deprecated 9.0-9.2` | ````markdown
{applies_to}`stack: deprecated 9.0-9.2`
```` | +| {applies_to}`stack: ga 9.0-9.2` | `` {applies_to}`stack: ga 9.0-9.2` `` | +| {applies_to}`stack: preview 9.0-9.2` | `` {applies_to}`stack: preview 9.0-9.2` `` | +| {applies_to}`stack: beta 9.0-9.1` | `` {applies_to}`stack: beta 9.0-9.1` `` | +| {applies_to}`stack: deprecated 9.0-9.2` | `` {applies_to}`stack: deprecated 9.0-9.2` `` | ### Exact version (=x.x) | Badge | Raw Markdown | |-------|--------------| -| {applies_to}`stack: ga =9.1` | ````markdown
{applies_to}`stack: ga =9.1`
```` | -| {applies_to}`stack: preview =9.0` | ````markdown
{applies_to}`stack: preview =9.0`
```` | -| {applies_to}`stack: beta =9.1` | ````markdown
{applies_to}`stack: beta =9.1`
```` | -| {applies_to}`stack: deprecated =9.0` | ````markdown
{applies_to}`stack: deprecated =9.0`
```` | -| {applies_to}`stack: removed =9.0` | ````markdown
{applies_to}`stack: removed =9.0`
```` | +| {applies_to}`stack: ga =9.1` | `` {applies_to}`stack: ga =9.1` `` | +| {applies_to}`stack: preview =9.0` | `` {applies_to}`stack: preview =9.0` `` | +| {applies_to}`stack: beta =9.1` | `` {applies_to}`stack: beta =9.1` `` | +| {applies_to}`stack: deprecated =9.0` | `` {applies_to}`stack: deprecated =9.0` `` | +| {applies_to}`stack: removed =9.0` | `` {applies_to}`stack: removed =9.0` `` | ### Multiple lifecycles | Badge | Raw Markdown | |-------|--------------| -| {applies_to}`stack: ga 9.2+, beta 9.0-9.1` | ````markdown
{applies_to}`stack: ga 9.2+, beta 9.0-9.1`
```` | -| {applies_to}`stack: ga 9.2+, preview 9.0-9.1` | ````markdown
{applies_to}`stack: ga 9.2+, preview 9.0-9.1`
```` | +| {applies_to}`stack: ga 9.2+, beta 9.0-9.1` | `` {applies_to}`stack: ga 9.2+, beta 9.0-9.1` `` | +| {applies_to}`stack: ga 9.2+, preview 9.0-9.1` | `` {applies_to}`stack: ga 9.2+, preview 9.0-9.1` `` | ### Deployment types | Badge | Raw Markdown | |-------|--------------| -| {applies_to}`ece: ga 9.0+` | ````markdown
{applies_to}`ece: ga 9.0+`
```` | -| {applies_to}`eck: preview 9.1+` | ````markdown
{applies_to}`eck: preview 9.1+`
```` | -| {applies_to}`ece: deprecated 6.7+` | ````markdown
{applies_to}`ece: deprecated 6.7+`
```` | -| {applies_to}`ece: removed` | ````markdown
{applies_to}`ece: removed`
```` | +| {applies_to}`ece: ga 9.0+` | `` {applies_to}`ece: ga 9.0+` `` | +| {applies_to}`eck: preview 9.1+` | `` {applies_to}`eck: preview 9.1+` `` | +| {applies_to}`ece: deprecated 6.7+` | `` {applies_to}`ece: deprecated 6.7+` `` | +| {applies_to}`ece: removed` | `` {applies_to}`ece: removed` `` | From 11580bdcb5372c2f21eb9fa1e43c09df8544810d Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 8 Dec 2025 14:27:07 -0300 Subject: [PATCH 13/61] Products with versions should show their base versions in badges without a specified version --- docs/testing/req.md | 20 +++++++++++++ .../AppliesTo/ApplicableToYamlConverter.cs | 4 +-- src/Elastic.Documentation/SemVersion.cs | 2 +- src/Elastic.Documentation/VersionSpec.cs | 2 +- .../Myst/Components/ApplicabilityRenderer.cs | 29 +++++++------------ .../Applicability/ApplicableToComponent.fs | 12 +++++++- tests/authoring/Blocks/Admonitions.fs | 27 ++++++++++++----- 7 files changed, 64 insertions(+), 32 deletions(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index 9ebb6e4ff..6e5b83337 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -59,6 +59,17 @@ Below is a table of `applies_to` badge scenarios. | {applies_to}`stack: deprecated` | `` {applies_to}`stack: deprecated` `` | | {applies_to}`stack: removed` | `` {applies_to}`stack: removed` `` | +### No version specified (product) + +| Badge | Raw Markdown | +|-------|--------------| +| {applies_to}`apm_agent_python: ga` | `` {applies_to}`apm_agent_python: ga` `` | +| {applies_to}`apm_agent_python: preview` | `` {applies_to}`apm_agent_python: preview` `` | +| {applies_to}`apm_agent_python: beta` | `` {applies_to}`apm_agent_python: beta` `` | +| {applies_to}`apm_agent_python: deprecated` | `` {applies_to}`apm_agent_python: deprecated` `` | +| {applies_to}`apm_agent_python: removed` | `` {applies_to}`apm_agent_python: removed` `` | + + ### Greater than or equal to (x.x+ / x.x) | Badge | Raw Markdown | @@ -69,6 +80,13 @@ Below is a table of `applies_to` badge scenarios. | {applies_to}`stack: beta 9.1+` | `` {applies_to}`stack: beta 9.1+` `` | | {applies_to}`stack: deprecated 9.0+` | `` {applies_to}`stack: deprecated 9.0+` `` | | {applies_to}`stack: removed 9.0` | `` {applies_to}`stack: removed 9.0` `` | +| {applies_to}`apm_agent_python: ga 6.0` | `` {applies_to}`apm_agent_python: ga 6.0` `` | +| {applies_to}`apm_agent_python: ga 6.5+` | `` {applies_to}`apm_agent_python: ga 6.5+` `` | +| {applies_to}`apm_agent_python: preview 6.24+` | `` {applies_to}`apm_agent_python: preview 6.24+` `` | +| {applies_to}`apm_agent_python: beta 6.1+` | `` {applies_to}`apm_agent_python: beta 6.1+` `` | +| {applies_to}`apm_agent_python: deprecated 6.0+` | `` {applies_to}`apm_agent_python: deprecated 6.0+` `` | +| {applies_to}`apm_agent_python: removed 6.0` | `` {applies_to}`apm_agent_python: removed 6.0` `` | + ### Range (x.x-y.y) @@ -78,6 +96,7 @@ Below is a table of `applies_to` badge scenarios. | {applies_to}`stack: preview 9.0-9.2` | `` {applies_to}`stack: preview 9.0-9.2` `` | | {applies_to}`stack: beta 9.0-9.1` | `` {applies_to}`stack: beta 9.0-9.1` `` | | {applies_to}`stack: deprecated 9.0-9.2` | `` {applies_to}`stack: deprecated 9.0-9.2` `` | +| {applies_to}`apm_agent_python: ga 6.0-6.23` | `` {applies_to}`apm_agent_python: ga 6.0-6.23` `` | ### Exact version (=x.x) @@ -88,6 +107,7 @@ Below is a table of `applies_to` badge scenarios. | {applies_to}`stack: beta =9.1` | `` {applies_to}`stack: beta =9.1` `` | | {applies_to}`stack: deprecated =9.0` | `` {applies_to}`stack: deprecated =9.0` `` | | {applies_to}`stack: removed =9.0` | `` {applies_to}`stack: removed =9.0` `` | +| {applies_to}`apm_agent_python: ga =6.20` | `` {applies_to}`apm_agent_python: ga =6.20` `` | ### Multiple lifecycles diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs index 3db7a6247..d86e06541 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs @@ -333,8 +333,8 @@ private static bool CheckVersionOverlap(VersionSpec v1, VersionSpec v2, out stri var (v1Min, v1Max) = GetEffectiveRange(v1); var (v2Min, v2Max) = GetEffectiveRange(v2); - var overlaps = v1Min.CompareTo(v2Max ?? new SemVersion(99999, 99999, 99999)) <= 0 && - v2Min.CompareTo(v1Max ?? new SemVersion(99999, 99999, 99999)) <= 0; + var overlaps = v1Min.CompareTo(v2Max ?? new SemVersion(99999, 0, 0)) <= 0 && + v2Min.CompareTo(v1Max ?? new SemVersion(99999, 0, 0)) <= 0; if (overlaps) message = $"Version ranges overlap."; diff --git a/src/Elastic.Documentation/SemVersion.cs b/src/Elastic.Documentation/SemVersion.cs index 8ee4f2cec..e1cb736da 100644 --- a/src/Elastic.Documentation/SemVersion.cs +++ b/src/Elastic.Documentation/SemVersion.cs @@ -8,7 +8,7 @@ namespace Elastic.Documentation; -public class AllVersions() : SemVersion(99999, 99999, 99999) +public class AllVersions() : SemVersion(99999, 0, 0) { public static AllVersions Instance { get; } = new(); } diff --git a/src/Elastic.Documentation/VersionSpec.cs b/src/Elastic.Documentation/VersionSpec.cs index 2cfb66774..abf80d168 100644 --- a/src/Elastic.Documentation/VersionSpec.cs +++ b/src/Elastic.Documentation/VersionSpec.cs @@ -8,7 +8,7 @@ namespace Elastic.Documentation; public sealed class AllVersionsSpec : VersionSpec { - private static readonly SemVersion AllVersionsSemVersion = new(9999, 9999, 9999); + private static readonly SemVersion AllVersionsSemVersion = new(99999, 0, 0); private AllVersionsSpec() : base(AllVersionsSemVersion, null, VersionSpecKind.GreaterThanOrEqual) { diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 6895236fb..f223ae7b8 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -36,21 +36,14 @@ public ApplicabilityRenderData RenderApplicability( var showLifecycle = applicability.Lifecycle != ProductLifecycle.GenerallyAvailable && string.IsNullOrEmpty(badgeLifecycleText); // Determine if we should show version based on VersionSpec - var showVersion = false; - var versionDisplay = string.Empty; + var versionDisplay = GetBadgeVersionText(applicability.Version, versioningSystem); + var showVersion = !string.IsNullOrEmpty(versionDisplay); - if (applicability.Version is not null && applicability.Version != AllVersionsSpec.Instance) + // Special handling for Removed lifecycle - don't show + suffix + if (applicability is { Lifecycle: ProductLifecycle.Removed, Version.Kind: VersionSpecKind.GreaterThanOrEqual } && + !string.IsNullOrEmpty(versionDisplay)) { - versionDisplay = GetBadgeVersionText(applicability.Version, versioningSystem); - showVersion = !string.IsNullOrEmpty(versionDisplay); - - // Special handling for Removed lifecycle - don't show + suffix - if (applicability.Lifecycle == ProductLifecycle.Removed && - applicability.Version.Kind == VersionSpecKind.GreaterThanOrEqual && - !string.IsNullOrEmpty(versionDisplay)) - { - versionDisplay = versionDisplay.TrimEnd('+'); - } + versionDisplay = versionDisplay.TrimEnd('+'); } return new ApplicabilityRenderData( @@ -261,13 +254,11 @@ private static string BuildBadgeLifecycleText( private static string GetBadgeVersionText(VersionSpec? versionSpec, VersioningSystem versioningSystem) { // When no version is specified, check if we should show the base version - if (versionSpec is null || versionSpec == AllVersionsSpec.Instance) + if (versionSpec is null) { - if (versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major) - return $"{versioningSystem.Base.Major}.{versioningSystem.Base.Minor}+"; - - // Otherwise, this is an unversioned product, show no version - return string.Empty; + return versioningSystem.Base != AllVersionsSpec.Instance.Min + ? $"{versioningSystem.Base.Major}.{versioningSystem.Base.Minor}+" + : string.Empty; // Otherwise, this is an unversioned product, show no version } var kind = versionSpec.Kind; diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index af138871e..25a9349fa 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -146,7 +146,11 @@ stack: ga 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."> Stack - + + + + 8.0+ +

@@ -484,6 +488,9 @@ stack: unavailable Unavailable + + 8.0+ +
@@ -503,6 +510,9 @@ product: ga + + 8.0+ +

diff --git a/tests/authoring/Blocks/Admonitions.fs b/tests/authoring/Blocks/Admonitions.fs index 324f3c5b4..85e738a23 100644 --- a/tests/authoring/Blocks/Admonitions.fs +++ b/tests/authoring/Blocks/Admonitions.fs @@ -68,10 +68,14 @@ This is a custom admonition with applies_to information. 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."> Stack - - -
-
+ + + + 8.0+ + + +
+
@@ -84,10 +88,14 @@ If this functionality is unavailable or behaves differently when deployed on ECH Serverless - - - - + + + + 8.0+ + + + +
@@ -105,6 +113,9 @@ This functionality may be changed or removed in a future release. Elastic will w Preview + + 8.0+ + From 56af075b0957c222508e036fac0eed4919fa63a3 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 11 Dec 2025 13:52:28 -0300 Subject: [PATCH 14/61] Introduce implicit semantics for multiple lifecycles --- .../AppliesTo/Applicability.cs | 75 +++++++++++++++++++ src/Elastic.Documentation/VersionSpec.cs | 15 ++++ .../Applicability/ApplicableToComponent.fs | 5 +- .../Applicability/AppliesToFrontMatter.fs | 5 +- tests/authoring/Inline/AppliesToRole.fs | 15 ++-- 5 files changed, 103 insertions(+), 12 deletions(-) diff --git a/src/Elastic.Documentation/AppliesTo/Applicability.cs b/src/Elastic.Documentation/AppliesTo/Applicability.cs index 2d5abbff9..0c8ae9d4a 100644 --- a/src/Elastic.Documentation/AppliesTo/Applicability.cs +++ b/src/Elastic.Documentation/AppliesTo/Applicability.cs @@ -37,6 +37,9 @@ public static bool TryParse(string? value, IList<(Severity, string)> diagnostics if (applications.Count == 0) return false; + // Infer version semantics when multiple items have GreaterThanOrEqual versions + applications = InferVersionSemantics(applications); + // Sort by version in descending order (the highest version first) // Items without versions (AllVersionsSpec.Instance) are sorted last var sortedApplications = applications.OrderDescending().ToArray(); @@ -44,6 +47,78 @@ public static bool TryParse(string? value, IList<(Severity, string)> diagnostics return true; } + /// + /// Infers versioning semantics according to the following ruleset: + /// - The highest version keeps GreaterThanOrEqual (e.g., 9.4+) + /// - Lower versions become Exact if consecutive, or Range to fill gaps + /// - This rule only applies when all versions are at minor level (patch = 0). + /// + private static List InferVersionSemantics(List applications) + { + // Get items with actual GreaterThanOrEqual versions (not AllVersionsSpec, not null, not ranges/exact) + var gteItems = applications + .Where(a => a.Version is { Kind: VersionSpecKind.GreaterThanOrEqual } + && a.Version != AllVersionsSpec.Instance) + .ToList(); + + // If 0 or 1 GTE items, no inference needed + if (gteItems.Count <= 1) + return applications; + + // Only apply inference when all entries are on patch version 0 + if (gteItems.Any(a => a.Version!.Min.Patch != 0)) + return applications; + + // Sort GTE items by version ascending to process from lowest to highest + var sortedGteVersions = gteItems + .Select(a => a.Version!.Min) + .Distinct() + .OrderBy(v => v) + .ToList(); + + if (sortedGteVersions.Count <= 1) + return applications; + + var versionMapping = new Dictionary(); + + for (var i = 0; i < sortedGteVersions.Count; i++) + { + var currentVersion = sortedGteVersions[i]; + + if (i == sortedGteVersions.Count - 1) + { + // Highest version keeps GreaterThanOrEqual + versionMapping[currentVersion] = VersionSpec.GreaterThanOrEqual(currentVersion); + } + else + { + var nextVersion = sortedGteVersions[i + 1]; + + // Define an Exact or Range VersionSpec according to the numeric difference between lifecycles + if (currentVersion.Major == nextVersion.Major + && nextVersion.Minor == currentVersion.Minor + 1) + versionMapping[currentVersion] = VersionSpec.Exact(currentVersion); + else + { + var rangeEnd = new SemVersion(nextVersion.Major, nextVersion.Minor - 1, 0); + versionMapping[currentVersion] = VersionSpec.Range(currentVersion, rangeEnd); + } + } + } + + // Apply the mapping to create updated applications + return applications.Select(a => + { + if (a.Version is null || a.Version == AllVersionsSpec.Instance || a is not { Version.Kind: VersionSpecKind.GreaterThanOrEqual }) + return a; + + if (versionMapping.TryGetValue(a.Version.Min, out var newSpec)) + return a with { Version = newSpec }; + + return a; + }).ToList(); + } + public virtual bool Equals(AppliesCollection? other) { if ((object)this == other) diff --git a/src/Elastic.Documentation/VersionSpec.cs b/src/Elastic.Documentation/VersionSpec.cs index abf80d168..34558ef65 100644 --- a/src/Elastic.Documentation/VersionSpec.cs +++ b/src/Elastic.Documentation/VersionSpec.cs @@ -56,6 +56,21 @@ protected VersionSpec(SemVersion min, SemVersion? max, VersionSpecKind kind) Kind = kind; } + /// + /// Creates an Exact version spec from a SemVersion. + /// + public static VersionSpec Exact(SemVersion version) => new(version, null, VersionSpecKind.Exact); + + /// + /// Creates a Range version spec from two SemVersions. + /// + public static VersionSpec Range(SemVersion min, SemVersion max) => new(min, max, VersionSpecKind.Range); + + /// + /// Creates a GreaterThanOrEqual version spec from a SemVersion. + /// + public static VersionSpec GreaterThanOrEqual(SemVersion min) => new(min, null, VersionSpecKind.GreaterThanOrEqual); + /// /// Tries to parse a version specification string. /// Supports: x.x, x.x+, x.x.x, x.x.x+ (gte), x.x-y.y (range), =x.x (exact) diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index 25a9349fa..5619211fd 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -405,7 +405,7 @@ stack: ga 8.8.0, preview 8.1.0 """ [] - let ``renders GA planned when preview exists alongside GA`` () = + let ``renders Preview when GA and Preview both exist for an unreleased entry`` () = markdown |> convertsToHtml """

GA - 8.0+ + 8.0 diff --git a/tests/authoring/Applicability/AppliesToFrontMatter.fs b/tests/authoring/Applicability/AppliesToFrontMatter.fs index 2d1f02b95..95483372e 100644 --- a/tests/authoring/Applicability/AppliesToFrontMatter.fs +++ b/tests/authoring/Applicability/AppliesToFrontMatter.fs @@ -163,10 +163,7 @@ applies_to: [] let ``apply matches expected`` () = markdown |> appliesTo (ApplicableTo( - Product=AppliesCollection([ - Applicability.op_Explicit "removed 9.7"; - Applicability.op_Explicit "preview 9.5" - ] |> Array.ofList) + Product=AppliesCollection.op_Explicit "removed 9.7, preview 9.5" )) type ``lenient to defining types at top level`` () = diff --git a/tests/authoring/Inline/AppliesToRole.fs b/tests/authoring/Inline/AppliesToRole.fs index a4f60ba3d..e6a3f09a1 100644 --- a/tests/authoring/Inline/AppliesToRole.fs +++ b/tests/authoring/Inline/AppliesToRole.fs @@ -125,7 +125,7 @@ type ``parses multiple applies_to in one line`` () = type ``render 'GA Planned' if preview exists alongside ga`` () = static let markdown = Setup.Markdown """ -This is an inline {applies_to}`stack: preview 9.0, ga 9.1` element. +This is an inline {applies_to}`stack: preview 8.0, ga 8.1` element. """ [] @@ -133,7 +133,7 @@ This is an inline {applies_to}`stack: preview 9.0, ga 9.1` element. let directives = markdown |> converts "index.md" |> parses test <@ directives.Length = 1 @> directives |> appliesToDirective (ApplicableTo( - Stack=AppliesCollection.op_Explicit "ga 9.1, preview 9.0" + Stack=AppliesCollection.op_Explicit "ga 8.1, preview 8.0" )) [] @@ -141,17 +141,20 @@ This is an inline {applies_to}`stack: preview 9.0, ga 9.1` element. markdown |> convertsToHtml """

This is an inline - +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.

"> Stack Preview + + 8.0 + From 7d0f792ba2a445e2204c4ab145260fb716a91528 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 11 Dec 2025 14:22:16 -0300 Subject: [PATCH 15/61] Add more examples in docs --- docs/_snippets/applies_to-version.md | 65 +++++- .../_snippets/multiple-lifecycle-states.md | 42 +++- docs/syntax/_snippets/versioned-lifecycle.md | 28 ++- docs/testing/req.md | 185 +++++++++--------- 4 files changed, 211 insertions(+), 109 deletions(-) diff --git a/docs/_snippets/applies_to-version.md b/docs/_snippets/applies_to-version.md index e5e70f63b..ef6ed62be 100644 --- a/docs/_snippets/applies_to-version.md +++ b/docs/_snippets/applies_to-version.md @@ -1,15 +1,64 @@ `applies_to` accepts the following version formats: -* **Greater than or equal to**: `x.x+`, `x.x`, `x.x.x+`, `x.x.x` (default behavior when no operator specified) -* **Range (inclusive)**: `x.x-y.y`, `x.x.x-y.y.y`, `x.x-y.y.y`, `x.x.x-y.y` -* **Exact version**: `=x.x`, `=x.x.x` +### Version specifiers -**Version Display:** +You can use version specifiers to precisely control how versions are interpreted: -- Versions are always displayed as **Major.Minor** (e.g., `9.1`) in badges, regardless of the format used in source files. -- Each version represents the **latest patch** of that minor version (e.g., `9.1` means 9.1.0, 9.1.1, 9.1.6, etc.). -- The `+` symbol indicates "this version and later" (e.g., `9.1+` means 9.1.0 and all subsequent releases). -- Ranges show both versions (e.g., `9.0-9.2`) when both are released, or convert to `+` format if the end version is unreleased. +| Specifier | Syntax | Description | Example | +|-----------|--------|-------------|---------| +| Greater than or equal (default) | `x.x` `x.x+` `x.x.x` `x.x.x+` | Feature available from this version onwards | `ga 9.2+` or `ga 9.2` | +| Range (inclusive) | `x.x-y.y` `x.x.x-y.y.y` | Feature available only in this version range | `beta 9.0-9.1` | +| Exact version | `=x.x` `=x.x.x` | Feature available only in this specific version | `preview =9.0` | + +Regardless of the version format used in the source file, the version number is always rendered in the `Major.Minor` format in badges. + +:::{note} +The `+` suffix is optional for greater-than-or-equal syntax. Both `ga 9.2` and `ga 9.2+` have the same meaning. +::: + +### Examples + +```yaml +# Greater than or equal (feature available from 9.2 onwards) +stack: ga 9.2 +stack: ga 9.2+ + +# Range (feature was in beta from 9.0 to 9.1, then became GA) +stack: ga 9.2+, beta 9.0-9.1 + +# Exact version (feature was in preview only in 9.0) +stack: ga 9.1+, preview =9.0 +``` + +### Implicit version inference for multiple lifecycles {#implicit-version-inference} + +When you specify multiple lifecycles with simple versions (without explicit specifiers), the system automatically infers the version ranges: + +**Input:** +```yaml +stack: preview 9.0, alpha 9.1, beta 9.2, ga 9.4 +``` + +**Interpreted as:** +```yaml +stack: preview =9.0, alpha =9.1, beta 9.2-9.3, ga 9.4+ +``` + +The inference rules are: +1. **Consecutive versions**: If a lifecycle is immediately followed by another in the next minor version, it's treated as an **exact version** (`=x.x`). +2. **Non-consecutive versions**: If there's a gap between one lifecycle's version and the next lifecycle's version, it becomes a **range** from the start version to one version before the next lifecycle. +3. **Last lifecycle**: The highest versioned lifecycle is always treated as **greater-than-or-equal** (`x.x+`). + +This makes it easy to document features that evolve through multiple lifecycle stages. For example, a feature that goes through preview → beta → GA can be written simply as: + +```yaml +stack: preview 9.0, beta 9.1, ga 9.3 +``` + +Which is automatically interpreted as: +```yaml +stack: preview =9.0, beta 9.1-9.2, ga 9.3+ +``` :::{note} **Automatic Version Sorting**: When you specify multiple versions for the same product, the build system automatically sorts them in descending order (highest version first) regardless of the order you write them in the source file. For example, `stack: ga 9.1, beta 9.0, preview 8.18` will be displayed with the highest priority lifecycle and version first. Items without versions are sorted last. diff --git a/docs/syntax/_snippets/multiple-lifecycle-states.md b/docs/syntax/_snippets/multiple-lifecycle-states.md index bb0bedf40..8c9dcd069 100644 --- a/docs/syntax/_snippets/multiple-lifecycle-states.md +++ b/docs/syntax/_snippets/multiple-lifecycle-states.md @@ -1,12 +1,35 @@ -`applies_to` keys accept comma-separated values to specify lifecycle states for multiple product versions. For example: +`applies_to` keys accept comma-separated values to specify lifecycle states for multiple product versions. -* A feature is added in 9.1 as tech preview and becomes GA in 9.4: +When you specify multiple lifecycles with simple versions, the system automatically infers whether each version represents an exact version, a range, or an open-ended range. Refer to [Implicit version inference](/_snippets/applies_to-version.md#implicit-version-inference) for details. + +### Examples + +* A feature is added in 9.0 as tech preview and becomes GA in 9.1: + + ```yml + applies_to: + stack: preview 9.0, ga 9.1 + ``` + + The preview is automatically interpreted as `=9.0` (exact), and GA as `9.1+` (open-ended). + +* A feature goes through multiple stages before becoming GA: + + ```yml + applies_to: + stack: preview 9.0, beta 9.1, ga 9.3 + ``` + + Interpreted as: `preview =9.0`, `beta 9.1-9.2`, `ga 9.3+` + +* A feature is unavailable for one version, beta for another, preview for a range, then GA: ```yml applies_to: - stack: preview 9.1, ga 9.4 + stack: unavailable 9.0, beta 9.1, preview 9.2, ga 9.4 ``` + Interpreted as: `unavailable =9.0`, `beta =9.1`, `preview 9.2-9.3`, `ga 9.4+` * A feature is deprecated in ECE 4.0 and is removed in 4.8. At the same time, it has already been removed in {{ech}}: @@ -15,4 +38,17 @@ deployment: ece: deprecated 4.0, removed 4.8 ess: removed + ``` + + The deprecated lifecycle is interpreted as `4.0-4.7` (range until removal). + +* Use explicit specifiers when you need precise control: + + ```yml + applies_to: + # Explicit exact version + stack: preview =9.0, ga 9.1+ + + # Explicit range + stack: beta 9.0-9.1, ga 9.2+ ``` \ No newline at end of file diff --git a/docs/syntax/_snippets/versioned-lifecycle.md b/docs/syntax/_snippets/versioned-lifecycle.md index ae6af6fee..cfe372e69 100644 --- a/docs/syntax/_snippets/versioned-lifecycle.md +++ b/docs/syntax/_snippets/versioned-lifecycle.md @@ -7,6 +7,8 @@ --- ``` + This means the feature is available from version 9.3 onwards (equivalent to `ga 9.3+`). + * When a change is introduced as preview or beta, use `preview` or `beta` as value for the corresponding key within the `applies_to`: ``` @@ -16,6 +18,28 @@ --- ``` +* When a feature is available only in a specific version range, use the range syntax: + + ``` + --- + applies_to: + stack: beta 9.0-9.1, ga 9.2 + --- + ``` + + This means the feature was in beta from 9.0 to 9.1, then became GA in 9.2+. + +* When a feature was in a specific lifecycle for exactly one version, use the exact syntax: + + ``` + --- + applies_to: + stack: preview =9.0, ga 9.1 + --- + ``` + + This means the feature was in preview only in 9.0, then became GA in 9.1+. + * When a change introduces a deprecation, use `deprecated` as value for the corresponding key within the `applies_to`: ``` @@ -33,4 +57,6 @@ applies_to: stack: deprecated 9.1, removed 9.4 --- - ``` \ No newline at end of file + ``` + + With the implicit version inference, this is interpreted as `deprecated 9.1-9.3, removed 9.4+`. \ No newline at end of file diff --git a/docs/testing/req.md b/docs/testing/req.md index 6e5b83337..454255dd9 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -8,20 +8,102 @@ mapped_pages: --- # Requirements +This page demonstrates various `applies_to` version syntax examples. + +## Version specifier examples + +### Greater than or equal (default) + +```{applies_to} +stack: ga 9.0 +``` + +This is equivalent to `ga 9.0+` — the feature is available from version 9.0 onwards. + +### Explicit range + +```{applies_to} +stack: beta 9.0-9.1, ga 9.2 +``` + +The feature was in beta from 9.0 to 9.1 (inclusive), then became GA in 9.2+. + +### Exact version + ```{applies_to} stack: preview =9.0, ga 9.1 ``` -1. Select **Create** to create a new policy, or select **Edit** {icon}`pencil` to open an existing policy. -1. Select **Create** to create a new policy, or select **Edit** {icon}`logo_vulnerability_management` to open an existing policy. +The feature was in preview only in version 9.0 (exactly), then became GA in 9.1+. +## Implicit version inference examples -{applies_to}`stack: preview 9.0` This tutorial is based on Elasticsearch 9.0. -This tutorial is based on Elasticsearch 9.0. This tutorial is based on Elasticsearch 9.0. -This tutorial is based on Elasticsearch 9.0. +### Simple two-stage lifecycle -To follow this tutorial you will need to install the following components: +```{applies_to} +stack: preview 9.0, ga 9.1 +``` + +Interpreted as: `preview =9.0` (exact), `ga 9.1+` (open-ended). + +### Multi-stage lifecycle with consecutive versions + +```{applies_to} +stack: preview 9.0, beta 9.1, ga 9.2 +``` + +Interpreted as: `preview =9.0`, `beta =9.1`, `ga 9.2+`. + +### Multi-stage lifecycle with gaps + +```{applies_to} +stack: unavailable 9.0, beta 9.1, preview 9.2, ga 9.4 +``` +Interpreted as: `unavailable =9.0`, `beta =9.1`, `preview 9.2-9.3` (range to fill the gap), `ga 9.4+`. + +### Four stages with varying gaps + +```{applies_to} +stack: preview 9.0, beta 9.2, ga 9.5 +``` + +Interpreted as: `preview 9.0-9.1`, `beta 9.2-9.4`, `ga 9.5+`. + +## Inline examples + +{applies_to}`stack: preview 9.0` This feature is in preview in 9.0. + +{applies_to}`stack: beta 9.0-9.1` This feature was in beta from 9.0 to 9.1. + +{applies_to}`stack: ga 9.2+` This feature is generally available since 9.2. + +{applies_to}`stack: preview =9.0` This feature was in preview only in 9.0 (exact). + +## Deprecation and removal examples + +```{applies_to} +stack: deprecated 9.2, removed 9.5 +``` + +Interpreted as: `deprecated 9.2-9.4`, `removed 9.5+`. + +{applies_to}`stack: deprecated 9.0` This feature is deprecated starting in 9.0. + +{applies_to}`stack: removed 9.2` This feature was removed in 9.2. + +## Mixed deployment examples + +```{applies_to} +stack: ga 9.0 +deployment: + ece: ga 4.0 + eck: beta 3.0, ga 3.1 +``` + +## Additional content + +To follow this tutorial you will need to install the following components: - An installation of Elasticsearch, based on our hosted [Elastic Cloud](https://www.elastic.co/cloud) service (which includes a free trial period), or a self-hosted service that you run on your own computer. See the Install Elasticsearch section above for installation instructions. - A [Python](https://python.org) interpreter. Make sure it is a recent version, such as Python 3.8 or newer. @@ -32,95 +114,4 @@ The tutorial assumes that you have no previous knowledge of Elasticsearch or gen - The [Flask](https://flask.palletsprojects.com/) web framework for Python. - The command prompt or terminal application in your operating system. - {applies_to}`ece: removed` - -## Applies To Badge Scenarios - -Below is a table of `applies_to` badge scenarios. - -### No version specified (serverless) - -| Badge | Raw Markdown | -|-------|--------------| -| {applies_to}`serverless: ga` | `` {applies_to}`serverless: ga` `` | -| {applies_to}`serverless: preview` | `` {applies_to}`serverless: preview` `` | -| {applies_to}`serverless: beta` | `` {applies_to}`serverless: beta` `` | -| {applies_to}`serverless: deprecated` | `` {applies_to}`serverless: deprecated` `` | -| {applies_to}`serverless: removed` | `` {applies_to}`serverless: removed` `` | - -### No version specified (stack) - -| Badge | Raw Markdown | -|-------|--------------| -| {applies_to}`stack: ga` | `` {applies_to}`stack: ga` `` | -| {applies_to}`stack: preview` | `` {applies_to}`stack: preview` `` | -| {applies_to}`stack: beta` | `` {applies_to}`stack: beta` `` | -| {applies_to}`stack: deprecated` | `` {applies_to}`stack: deprecated` `` | -| {applies_to}`stack: removed` | `` {applies_to}`stack: removed` `` | - -### No version specified (product) - -| Badge | Raw Markdown | -|-------|--------------| -| {applies_to}`apm_agent_python: ga` | `` {applies_to}`apm_agent_python: ga` `` | -| {applies_to}`apm_agent_python: preview` | `` {applies_to}`apm_agent_python: preview` `` | -| {applies_to}`apm_agent_python: beta` | `` {applies_to}`apm_agent_python: beta` `` | -| {applies_to}`apm_agent_python: deprecated` | `` {applies_to}`apm_agent_python: deprecated` `` | -| {applies_to}`apm_agent_python: removed` | `` {applies_to}`apm_agent_python: removed` `` | - - -### Greater than or equal to (x.x+ / x.x) - -| Badge | Raw Markdown | -|-------|--------------| -| {applies_to}`stack: ga 9.1` | `` {applies_to}`stack: ga 9.1` `` | -| {applies_to}`stack: ga 9.1+` | `` {applies_to}`stack: ga 9.1+` `` | -| {applies_to}`stack: preview 9.0+` | `` {applies_to}`stack: preview 9.0+` `` | -| {applies_to}`stack: beta 9.1+` | `` {applies_to}`stack: beta 9.1+` `` | -| {applies_to}`stack: deprecated 9.0+` | `` {applies_to}`stack: deprecated 9.0+` `` | -| {applies_to}`stack: removed 9.0` | `` {applies_to}`stack: removed 9.0` `` | -| {applies_to}`apm_agent_python: ga 6.0` | `` {applies_to}`apm_agent_python: ga 6.0` `` | -| {applies_to}`apm_agent_python: ga 6.5+` | `` {applies_to}`apm_agent_python: ga 6.5+` `` | -| {applies_to}`apm_agent_python: preview 6.24+` | `` {applies_to}`apm_agent_python: preview 6.24+` `` | -| {applies_to}`apm_agent_python: beta 6.1+` | `` {applies_to}`apm_agent_python: beta 6.1+` `` | -| {applies_to}`apm_agent_python: deprecated 6.0+` | `` {applies_to}`apm_agent_python: deprecated 6.0+` `` | -| {applies_to}`apm_agent_python: removed 6.0` | `` {applies_to}`apm_agent_python: removed 6.0` `` | - - -### Range (x.x-y.y) - -| Badge | Raw Markdown | -|-------|--------------| -| {applies_to}`stack: ga 9.0-9.2` | `` {applies_to}`stack: ga 9.0-9.2` `` | -| {applies_to}`stack: preview 9.0-9.2` | `` {applies_to}`stack: preview 9.0-9.2` `` | -| {applies_to}`stack: beta 9.0-9.1` | `` {applies_to}`stack: beta 9.0-9.1` `` | -| {applies_to}`stack: deprecated 9.0-9.2` | `` {applies_to}`stack: deprecated 9.0-9.2` `` | -| {applies_to}`apm_agent_python: ga 6.0-6.23` | `` {applies_to}`apm_agent_python: ga 6.0-6.23` `` | - -### Exact version (=x.x) - -| Badge | Raw Markdown | -|-------|--------------| -| {applies_to}`stack: ga =9.1` | `` {applies_to}`stack: ga =9.1` `` | -| {applies_to}`stack: preview =9.0` | `` {applies_to}`stack: preview =9.0` `` | -| {applies_to}`stack: beta =9.1` | `` {applies_to}`stack: beta =9.1` `` | -| {applies_to}`stack: deprecated =9.0` | `` {applies_to}`stack: deprecated =9.0` `` | -| {applies_to}`stack: removed =9.0` | `` {applies_to}`stack: removed =9.0` `` | -| {applies_to}`apm_agent_python: ga =6.20` | `` {applies_to}`apm_agent_python: ga =6.20` `` | - -### Multiple lifecycles - -| Badge | Raw Markdown | -|-------|--------------| -| {applies_to}`stack: ga 9.2+, beta 9.0-9.1` | `` {applies_to}`stack: ga 9.2+, beta 9.0-9.1` `` | -| {applies_to}`stack: ga 9.2+, preview 9.0-9.1` | `` {applies_to}`stack: ga 9.2+, preview 9.0-9.1` `` | - -### Deployment types - -| Badge | Raw Markdown | -|-------|--------------| -| {applies_to}`ece: ga 9.0+` | `` {applies_to}`ece: ga 9.0+` `` | -| {applies_to}`eck: preview 9.1+` | `` {applies_to}`eck: preview 9.1+` `` | -| {applies_to}`ece: deprecated 6.7+` | `` {applies_to}`ece: deprecated 6.7+` `` | -| {applies_to}`ece: removed` | `` {applies_to}`ece: removed` `` | From 13301d08daa58969b707b37bf98b8a3028d716cf Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 11 Dec 2025 14:25:03 -0300 Subject: [PATCH 16/61] Typo --- docs/testing/req.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index 454255dd9..b4f71f821 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -62,7 +62,7 @@ stack: unavailable 9.0, beta 9.1, preview 9.2, ga 9.4 Interpreted as: `unavailable =9.0`, `beta =9.1`, `preview 9.2-9.3` (range to fill the gap), `ga 9.4+`. -### Four stages with varying gaps +### Three stages with varying gaps ```{applies_to} stack: preview 9.0, beta 9.2, ga 9.5 From 7cd899b1802825e953b3322f9e7cce1a51b69f65 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Thu, 11 Dec 2025 14:29:05 -0300 Subject: [PATCH 17/61] Fix interpretation of lifecycle - using ranges after current stack version mighyt cause confusion --- docs/testing/req.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index b4f71f821..21a16a3ba 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -65,10 +65,10 @@ Interpreted as: `unavailable =9.0`, `beta =9.1`, `preview 9.2-9.3` (range to fil ### Three stages with varying gaps ```{applies_to} -stack: preview 9.0, beta 9.2, ga 9.5 +stack: preview 8.0, beta 9.0, ga 9.2 ``` -Interpreted as: `preview 9.0-9.1`, `beta 9.2-9.4`, `ga 9.5+`. +Interpreted as: `preview 8.0-8.19`, `beta 9.0-9.1`, `ga 9.2+`. ## Inline examples From 072479e43416f1ada1e4a0dd82fc0b6631091b8d Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Fri, 12 Dec 2025 12:55:55 -0300 Subject: [PATCH 18/61] Change popup to a popover component, alongside static descriptions --- docs/testing/req.md | 2 +- src/Elastic.Documentation.Site/Assets/main.ts | 2 +- .../Assets/markdown/applies-to.css | 51 +- .../Assets/markdown/applies-to.ts | 23 - .../web-components/AppliesToPopover.tsx | 387 ++++++++++++++ .../AppliesTo/Applicability.cs | 14 +- .../AppliesTo/ProductLifecycleInfo.cs | 62 +++ .../Myst/Components/ApplicabilityRenderer.cs | 490 ++++++++++++------ .../Components/ApplicableToComponent.cshtml | 45 +- .../Myst/Components/ApplicableToViewModel.cs | 98 ++-- .../Myst/Components/LifecycleDescriptions.cs | 74 +++ .../Myst/Components/ProductDescriptions.cs | 192 +++++++ 12 files changed, 1137 insertions(+), 303 deletions(-) delete mode 100644 src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts create mode 100644 src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx create mode 100644 src/Elastic.Documentation/AppliesTo/ProductLifecycleInfo.cs create mode 100644 src/Elastic.Markdown/Myst/Components/LifecycleDescriptions.cs create mode 100644 src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs diff --git a/docs/testing/req.md b/docs/testing/req.md index 21a16a3ba..2c1c8f7a2 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -65,7 +65,7 @@ Interpreted as: `unavailable =9.0`, `beta =9.1`, `preview 9.2-9.3` (range to fil ### Three stages with varying gaps ```{applies_to} -stack: preview 8.0, beta 9.0, ga 9.2 +stack: preview 8.0, beta 9.1, ga 9.3 ``` Interpreted as: `preview 8.0-8.19`, `beta 9.0-9.1`, `ga 9.2+`. diff --git a/src/Elastic.Documentation.Site/Assets/main.ts b/src/Elastic.Documentation.Site/Assets/main.ts index 38f9fcfd6..0beab1d5e 100644 --- a/src/Elastic.Documentation.Site/Assets/main.ts +++ b/src/Elastic.Documentation.Site/Assets/main.ts @@ -2,7 +2,6 @@ import { initAppliesSwitch } from './applies-switch' import { initCopyButton } from './copybutton' import { initHighlight } from './hljs' import { initImageCarousel } from './image-carousel' -import './markdown/applies-to' import { openDetailsWithAnchor } from './open-details-with-anchor' import { initNav } from './pages-nav' import { initSmoothScroll } from './smooth-scroll' @@ -33,6 +32,7 @@ initializeOtel({ // Parcel will automatically code-split this into a separate chunk import('./web-components/SearchOrAskAi/SearchOrAskAi') import('./web-components/VersionDropdown') +import('./web-components/AppliesToPopover') const { getOS } = new UAParser() const isLazyLoadNavigationEnabled = diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css index d371819df..dc59efa8e 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css @@ -4,14 +4,35 @@ @apply text-subdued; - [data-tippy-content]:not([data-tippy-content='']) { - @apply cursor-help; + applies-to-popover { + display: contents; } .applicable-info { @apply border-grey-20 inline-flex cursor-default rounded-full border-[1px] bg-white pt-1.5 pr-3 pb-1.5 pl-3; } + .applicable-info--clickable { + @apply cursor-pointer; + + &:hover { + @apply border-grey-30 bg-grey-10; + } + + &:focus { + outline: none; + } + + &:focus-visible { + outline: 2px solid var(--color-blue-elastic); + outline-offset: 2px; + } + } + + .applicable-info--pinned { + @apply border-blue-elastic bg-grey-10; + } + .applicable-meta { @apply inline-flex gap-1.5; } @@ -35,6 +56,12 @@ .applies.applies-inline { display: inline-block; vertical-align: bottom; + + applies-to-popover { + display: inline-flex; + vertical-align: bottom; + } + .applicable-separator { margin-left: calc(var(--spacing) * 1.5); margin-right: calc(var(--spacing) * 1.5); @@ -57,19 +84,9 @@ } } -.tippy-box[data-theme~='applies-to'] { - .tippy-content { - white-space: normal; - - strong { - display: block; - margin-bottom: calc(var(--spacing) * 1); - } - } - - .tippy-content > div:not(:last-child) { - border-bottom: 1px dotted var(--color-grey-50); - padding-bottom: calc(var(--spacing) * 3); - margin-bottom: calc(var(--spacing) * 3); - } +.euiPopover__panel { + /* Shadow and border for the popover */ + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important; + border: 1px solid var(--color-grey-20) !important; + border-radius: 6px !important; } diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts deleted file mode 100644 index df5ad9d66..000000000 --- a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { $$ } from 'select-dom' -import tippy from 'tippy.js' - -document.addEventListener('htmx:load', function () { - const selector = [ - '.applies [data-tippy-content]:not([data-tippy-content=""])', - '.applies-inline [data-tippy-content]:not([data-tippy-content=""])', - ].join(', ') - - const appliesToBadgesWithTooltip = $$(selector) - appliesToBadgesWithTooltip.forEach((badge) => { - const content = badge.getAttribute('data-tippy-content') - if (!content) return - tippy(badge, { - content, - allowHTML: true, - delay: [400, 100], - hideOnClick: false, - ignoreAttributes: true, - theme: 'applies-to', - }) - }) -}) diff --git a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx new file mode 100644 index 000000000..09c3ab437 --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx @@ -0,0 +1,387 @@ +'use strict' + +import '../eui-icons-cache' +import { EuiPopover, useGeneratedHtmlId } from '@elastic/eui' +import { css } from '@emotion/react' +import r2wc from '@r2wc/react-to-web-component' +import * as React from 'react' +import { useState, useRef, useEffect, useCallback } from 'react' + +type AppliesToPopoverProps = { + badgeKey?: string + badgeLifecycleText?: string + badgeVersion?: string + lifecycleClass?: string + lifecycleName?: string + showLifecycleName?: boolean + showVersion?: boolean + hasMultipleLifecycles?: boolean + popoverContent?: string + showPopover?: boolean + isInline?: boolean +} + +const AppliesToPopover = ({ + badgeKey, + badgeLifecycleText, + badgeVersion, + lifecycleClass, + lifecycleName, + showLifecycleName, + showVersion, + hasMultipleLifecycles, + popoverContent, + showPopover = true, + isInline = false, +}: AppliesToPopoverProps) => { + const [isOpen, setIsOpen] = useState(false) + const [isPinned, setIsPinned] = useState(false) + const popoverId = useGeneratedHtmlId({ prefix: 'appliesToPopover' }) + const contentRef = useRef(null) + const badgeRef = useRef(null) + const hoverTimeoutRef = useRef | null>(null) + + const openPopover = useCallback(() => { + if (showPopover && popoverContent) { + setIsOpen(true) + } + }, [showPopover, popoverContent]) + + const closePopover = useCallback(() => { + if (!isPinned) { + setIsOpen(false) + } + }, [isPinned]) + + const handleClick = useCallback(() => { + if (showPopover && popoverContent) { + if (isPinned) { + // If already pinned, unpin and close + setIsPinned(false) + setIsOpen(false) + } else { + // Pin the popover open + setIsPinned(true) + setIsOpen(true) + } + } + }, [showPopover, popoverContent, isPinned]) + + const handleClosePopover = useCallback(() => { + setIsPinned(false) + setIsOpen(false) + }, []) + + const handleMouseEnter = useCallback(() => { + // Clear any pending close timeout + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current) + hoverTimeoutRef.current = null + } + openPopover() + }, [openPopover]) + + const handleMouseLeave = useCallback(() => { + // Small delay before closing to allow moving to the popover content + hoverTimeoutRef.current = setTimeout(() => { + closePopover() + }, 100) + }, [closePopover]) + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current) + } + } + }, []) + + // Close popover when badge becomes hidden (e.g., parent details element collapses) + useEffect(() => { + if (!badgeRef.current || !isOpen) return + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + // If badge is no longer visible, close the popover + if (!entry.isIntersecting) { + setIsPinned(false) + setIsOpen(false) + } + }) + }, + { threshold: 0 } + ) + + observer.observe(badgeRef.current) + + return () => { + observer.disconnect() + } + }, [isOpen]) + + // Handle details/summary elements for collapsible sections + useEffect(() => { + if (!contentRef.current) return + + const details = contentRef.current.querySelectorAll('details') + details.forEach((detail) => { + detail.addEventListener('toggle', (e) => { + e.stopPropagation() + }) + }) + }, [isOpen, popoverContent]) + + const showSeparator = + badgeKey && (showLifecycleName || showVersion || badgeLifecycleText) + + const badgeButton = ( + { + if ( + showPopover && + popoverContent && + (e.key === 'Enter' || e.key === ' ') + ) { + e.preventDefault() + handleClick() + } + }} + > + {badgeKey} + + {showSeparator && } + + + {showLifecycleName && ( + + {lifecycleName} + + )} + {showVersion ? ( + + {badgeVersion} + + ) : ( + badgeLifecycleText + )} + {hasMultipleLifecycles && ( + + + + + + )} + + + ) + + if (!showPopover || !popoverContent) { + return badgeButton + } + + return ( + +
+ + ) +} + +customElements.define( + 'applies-to-popover', + r2wc(AppliesToPopover, { + props: { + badgeKey: 'string', + badgeLifecycleText: 'string', + badgeVersion: 'string', + lifecycleClass: 'string', + lifecycleName: 'string', + showLifecycleName: 'boolean', + showVersion: 'boolean', + hasMultipleLifecycles: 'boolean', + popoverContent: 'string', + showPopover: 'boolean', + isInline: 'boolean', + }, + }) +) + +export default AppliesToPopover diff --git a/src/Elastic.Documentation/AppliesTo/Applicability.cs b/src/Elastic.Documentation/AppliesTo/Applicability.cs index 0c8ae9d4a..7eee7debb 100644 --- a/src/Elastic.Documentation/AppliesTo/Applicability.cs +++ b/src/Elastic.Documentation/AppliesTo/Applicability.cs @@ -183,19 +183,7 @@ public record Applicability : IComparable, IComparable public string GetLifeCycleName() => - Lifecycle switch - { - ProductLifecycle.TechnicalPreview => "Preview", - ProductLifecycle.Beta => "Beta", - ProductLifecycle.Development => "Development", - ProductLifecycle.Deprecated => "Deprecated", - ProductLifecycle.Planned => "Planned", - ProductLifecycle.Discontinued => "Discontinued", - ProductLifecycle.Unavailable => "Unavailable", - ProductLifecycle.GenerallyAvailable => "GA", - ProductLifecycle.Removed => "Removed", - _ => throw new ArgumentOutOfRangeException(nameof(Lifecycle), Lifecycle, null) - }; + ProductLifecycleInfo.GetShortName(Lifecycle); /// diff --git a/src/Elastic.Documentation/AppliesTo/ProductLifecycleInfo.cs b/src/Elastic.Documentation/AppliesTo/ProductLifecycleInfo.cs new file mode 100644 index 000000000..811709f43 --- /dev/null +++ b/src/Elastic.Documentation/AppliesTo/ProductLifecycleInfo.cs @@ -0,0 +1,62 @@ +// 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; + +/// +/// Provides consolidated metadata for product lifecycle states. +/// +public static class ProductLifecycleInfo +{ + /// + /// Contains all display and ordering information for a lifecycle state. + /// + /// Short name for badges (e.g., "Preview", "Beta", "GA"). + /// Full display text for popovers (e.g., "Generally available", "Preview"). + /// Priority order for sorting (lower = higher priority, GA=0). + public sealed record LifecycleMetadata(string ShortName, string DisplayText, int Order); + + /// + /// Gets the metadata for a given lifecycle state. + /// + public static LifecycleMetadata GetMetadata(ProductLifecycle lifecycle) => + Metadata.GetValueOrDefault(lifecycle, FallbackMetadata); + + /// + /// Gets the short name for a lifecycle (e.g., "Preview", "Beta", "GA"). + /// Used for badge CSS classes and compact display. + /// + public static string GetShortName(ProductLifecycle lifecycle) => + GetMetadata(lifecycle).ShortName; + + /// + /// Gets the full display text for a lifecycle (e.g., "Generally available", "Preview"). + /// Used in popover availability text. + /// + public static string GetDisplayText(ProductLifecycle lifecycle) => + GetMetadata(lifecycle).DisplayText; + + /// + /// Gets the sort order for a lifecycle (lower = higher priority). + /// GA=0, Beta=1, Preview=2, etc. + /// + public static int GetOrder(ProductLifecycle lifecycle) => + GetMetadata(lifecycle).Order; + + private static readonly LifecycleMetadata FallbackMetadata = new("", "", 999); + + private static readonly Dictionary Metadata = new() + { + [ProductLifecycle.GenerallyAvailable] = new("GA", "Generally available", 0), + [ProductLifecycle.Beta] = new("Beta", "Beta", 1), + [ProductLifecycle.TechnicalPreview] = new("Preview", "Preview", 2), + [ProductLifecycle.Planned] = new("Planned", "Planned", 3), + [ProductLifecycle.Deprecated] = new("Deprecated", "Deprecated", 4), + [ProductLifecycle.Removed] = new("Removed", "Removed", 5), + [ProductLifecycle.Unavailable] = new("Unavailable", "Unavailable", 6), + [ProductLifecycle.Development] = new("Development", "Development", 7), + [ProductLifecycle.Discontinued] = new("Discontinued", "Discontinued", 8), + }; +} + diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index f223ae7b8..2f5c621e7 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -21,16 +21,61 @@ public record ApplicabilityRenderData( bool HasMultipleLifecycles = false ); - public ApplicabilityRenderData RenderApplicability( - Applicability applicability, + public static ApplicabilityRenderData RenderApplicability( + IEnumerable applicabilities, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, + VersioningSystem versioningSystem) + { + var applicabilityList = applicabilities.ToList(); + var allApplications = new AppliesCollection([.. applicabilityList]); + + // Sort by lifecycle priority (GA > Beta > Preview > etc.) to determine display order + var sortedApplicabilities = applicabilityList + .OrderBy(a => ProductLifecycleInfo.GetOrder(a.Lifecycle)) + .ThenByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) + .ToList(); + + var primaryLifecycle = sortedApplicabilities.First(); + var primaryBadgeData = GetBadgeData(primaryLifecycle, versioningSystem, allApplications); + + // If the primary lifecycle returns an empty badge text (indicating "use previous lifecycle") + // and we have multiple lifecycles, use the next lifecycle in priority order + var applicabilityToDisplay = string.IsNullOrEmpty(primaryBadgeData.BadgeLifecycleText) && + string.IsNullOrEmpty(primaryBadgeData.Version) && + sortedApplicabilities.Count >= 2 + ? sortedApplicabilities[1] + : primaryLifecycle; + + var badgeData = applicabilityToDisplay == primaryLifecycle + ? primaryBadgeData + : GetBadgeData(applicabilityToDisplay, versioningSystem, allApplications); + + var popoverContent = BuildPopoverContent(applicabilityList, applicabilityDefinition, versioningSystem); + + // Check if there are multiple different lifecycles + var hasMultipleLifecycles = applicabilityList.Select(a => a.Lifecycle).Distinct().Count() > 1; + + return new ApplicabilityRenderData( + BadgeLifecycleText: badgeData.BadgeLifecycleText, + Version: badgeData.Version, + TooltipText: popoverContent, + LifecycleClass: badgeData.LifecycleClass, + LifecycleName: badgeData.LifecycleName, + ShowLifecycleName: badgeData.ShowLifecycleName || (string.IsNullOrEmpty(badgeData.BadgeLifecycleText) && hasMultipleLifecycles), + ShowVersion: badgeData.ShowVersion, + HasMultipleLifecycles: hasMultipleLifecycles + ); + } + + /// + /// Gets the badge display data for a single applicability (used internally for badge rendering decisions). + /// + private static BadgeData GetBadgeData( + Applicability applicability, VersioningSystem versioningSystem, AppliesCollection allApplications) { var lifecycleClass = applicability.GetLifeCycleName().ToLowerInvariant().Replace(" ", "-"); - var lifecycleFull = GetLifecycleFullText(applicability.Lifecycle); - - var tooltipText = BuildTooltipText(applicability, applicabilityDefinition, versioningSystem, lifecycleFull); var badgeLifecycleText = BuildBadgeLifecycleText(applicability, versioningSystem, allApplications); var showLifecycle = applicability.Lifecycle != ProductLifecycle.GenerallyAvailable && string.IsNullOrEmpty(badgeLifecycleText); @@ -46,10 +91,9 @@ public ApplicabilityRenderData RenderApplicability( versionDisplay = versionDisplay.TrimEnd('+'); } - return new ApplicabilityRenderData( + return new BadgeData( BadgeLifecycleText: badgeLifecycleText, Version: versionDisplay, - TooltipText: tooltipText, LifecycleClass: lifecycleClass, LifecycleName: applicability.GetLifeCycleName(), ShowLifecycleName: showLifecycle, @@ -57,54 +101,34 @@ public ApplicabilityRenderData RenderApplicability( ); } - public ApplicabilityRenderData RenderCombinedApplicability( - IEnumerable applicabilities, - ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, - VersioningSystem versioningSystem, - AppliesCollection allApplications) - { - var applicabilityList = applicabilities.ToList(); - - // Sort by lifecycle priority (GA > Beta > Preview > etc.) to determine display order - var sortedApplicabilities = applicabilityList - .OrderBy(a => GetLifecycleOrder(a.Lifecycle)) - .ThenByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) - .ToList(); - - var primaryLifecycle = sortedApplicabilities.First(); - - var primaryRender = RenderApplicability(primaryLifecycle, applicabilityDefinition, versioningSystem, allApplications); - - // If the primary lifecycle returns an empty badge text (indicating "use previous lifecycle") - // and we have multiple lifecycles, use the next lifecycle in priority order - var applicabilityToDisplay = string.IsNullOrEmpty(primaryRender.BadgeLifecycleText) && - string.IsNullOrEmpty(primaryRender.Version) && - sortedApplicabilities.Count >= 2 - ? sortedApplicabilities[1] - : primaryLifecycle; - - var primaryRenderData = RenderApplicability(applicabilityToDisplay, applicabilityDefinition, versioningSystem, allApplications); - var combinedTooltip = BuildCombinedTooltipText(applicabilityList, applicabilityDefinition, versioningSystem); - - // Check if there are multiple different lifecycles - var hasMultipleLifecycles = applicabilityList.Select(a => a.Lifecycle).Distinct().Count() > 1; - - return primaryRenderData with - { - TooltipText = combinedTooltip, - HasMultipleLifecycles = hasMultipleLifecycles, - ShowLifecycleName = primaryRenderData.ShowLifecycleName || (string.IsNullOrEmpty(primaryRenderData.BadgeLifecycleText) && hasMultipleLifecycles) - }; - } + private sealed record BadgeData( + string BadgeLifecycleText, + string Version, + string LifecycleClass, + string LifecycleName, + bool ShowLifecycleName, + bool ShowVersion + ); - private static string BuildCombinedTooltipText( + private static string BuildPopoverContent( List applicabilities, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, VersioningSystem versioningSystem) { - var tooltipParts = new List(); + var productInfo = ProductDescriptions.GetProductInfo(versioningSystem.Id); + var productName = GetPlainProductName(applicabilityDefinition.DisplayName); + + var parts = new List(); - // Order by the same logic as primary selection: available first (by version desc), then future (by version asc) + // Product description + if (productInfo is not null && !string.IsNullOrEmpty(productInfo.Description)) + { + // language=html + parts.Add($"

{productInfo.Description}

"); + } + + // Availability section - collect items from all applicabilities + // Order by: available first (by version desc), then future (by version asc) var orderedApplicabilities = applicabilities .OrderByDescending(a => a.Version is null || a.Version is AllVersionsSpec || (a.Version is { } vs && vs.Min <= versioningSystem.Current) ? 1 : 0) @@ -112,96 +136,250 @@ private static string BuildCombinedTooltipText( .ThenBy(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) .ToList(); + var allAvailabilityItems = new List(); foreach (var applicability in orderedApplicabilities) { - var lifecycleFull = GetLifecycleFullText(applicability.Lifecycle); - var heading = CreateApplicabilityHeading(applicability, applicabilityDefinition); - var tooltipText = BuildTooltipText(applicability, applicabilityDefinition, versioningSystem, lifecycleFull); + var items = BuildAvailabilityItems(applicability, versioningSystem, productName, applicabilities.Count); + allAvailabilityItems.AddRange(items); + } + + if (allAvailabilityItems.Count > 0) + { + // language=html + parts.Add("

Availability

"); + parts.Add("

The functionality described here is:

"); + parts.Add(string.Join("\n", allAvailabilityItems)); + } + + // Additional availability info + if (productInfo is { AdditionalAvailabilityInfo: not null }) + { // language=html - tooltipParts.Add($"
{heading}{tooltipText}
"); + parts.Add($"

{productInfo.AdditionalAvailabilityInfo}

"); } - return string.Join("\n\n", tooltipParts); + // Version note + if (productInfo is { IncludeVersionNote: true } && versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major) + { + // language=html + parts.Add($"

{ProductDescriptions.VersionNote}

"); + } + + return string.Join("\n", parts); } - private static string CreateApplicabilityHeading(Applicability applicability, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition) + /// + /// Builds the availability list items (details/summary elements) for an applicability. + /// + private static List BuildAvailabilityItems( + Applicability applicability, + VersioningSystem versioningSystem, + string productName, + int lifecycleCount) { - var lifecycleName = applicability.GetLifeCycleName(); - var versionText = applicability.Version is not null ? $" {applicability.Version.Min}" : ""; - // language=html - return $"""{applicabilityDefinition.DisplayName} {lifecycleName}{versionText}:"""; + var items = new List(); + + var availabilityText = GenerateAvailabilityText(applicability, versioningSystem, lifecycleCount); + + if (availabilityText is null) + return items; + + var isReleased = IsVersionReleased(applicability, versioningSystem); + var lifecycleDescription = LifecycleDescriptions.GetDescriptionWithProduct( + applicability.Lifecycle, + isReleased, + productName + ); + + // Build the details/summary element for collapsible lifecycle description + if (!string.IsNullOrEmpty(lifecycleDescription)) + { + // language=html + items.Add($""" +
+{availabilityText} +

{lifecycleDescription}

+
+"""); + } + else + { + // No collapsible content, just show the text + // language=html + items.Add($"

{availabilityText}

"); + } + + return items; } - private static string GetLifecycleFullText(ProductLifecycle lifecycle) => lifecycle switch - { - ProductLifecycle.GenerallyAvailable => "Available", - ProductLifecycle.Beta => "Available in beta", - ProductLifecycle.TechnicalPreview => "Available in technical preview", - ProductLifecycle.Deprecated => "Deprecated", - ProductLifecycle.Removed => "Removed", - ProductLifecycle.Unavailable => "Unavailable", - _ => "" - }; - - private static string BuildTooltipText( + /// + /// Generates the dynamic availability text based on version type, lifecycle, release status, and lifecycle count. + /// Returns null if the item should not be added to the availability list. + /// + private static string? GenerateAvailabilityText( Applicability applicability, - ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, VersioningSystem versioningSystem, - string lifecycleFull) + int lifecycleCount) { - var tooltipText = ""; + var lifecycle = applicability.Lifecycle; + var versionSpec = applicability.Version; - // Check if a specific version is provided - if (applicability.Version is not null && applicability.Version != AllVersionsSpec.Instance) + // No version (null or AllVersionsSpec) with unversioned product + if ((versionSpec is null || versionSpec is AllVersionsSpec) && + versioningSystem.Base.Major == AllVersionsSpec.Instance.Min.Major) { - tooltipText = applicability.Version.Min <= versioningSystem.Current - ? $"{lifecycleFull} on {applicabilityDefinition.DisplayName} version {applicability.Version.Min} and later unless otherwise specified." - : applicability.Lifecycle switch - { - ProductLifecycle.GenerallyAvailable - or ProductLifecycle.Beta - or ProductLifecycle.TechnicalPreview - or ProductLifecycle.Planned => - $"We plan to add this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.", - ProductLifecycle.Deprecated => - $"We plan to deprecate this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.", - ProductLifecycle.Removed => - $"We plan to remove this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.", - _ => tooltipText - }; + return ProductLifecycleInfo.GetDisplayText(lifecycle); } - else + + // No version with versioned product + if (versionSpec is null or AllVersionsSpec) { - // No version specified - check if we should show base version - tooltipText = versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major - ? applicability.Lifecycle switch - { - ProductLifecycle.Removed => - $"Removed in {applicabilityDefinition.DisplayName} {versioningSystem.Base.Major}.{versioningSystem.Base.Minor}.", - _ => - $"{lifecycleFull} since {versioningSystem.Base.Major}.{versioningSystem.Base.Minor}." - } - : $"{lifecycleFull} on {applicabilityDefinition.DisplayName} unless otherwise specified."; + var baseVersion = $"{versioningSystem.Base.Major}.{versioningSystem.Base.Minor}"; + return lifecycle switch + { + ProductLifecycle.Removed => $"Removed in {baseVersion}", + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} since {baseVersion}" + }; + } + + // Get version info + var min = versionSpec.Min; + var max = versionSpec.Max; + var minVersion = $"{min.Major}.{min.Minor}"; + var maxVersion = max is not null ? $"{max.Major}.{max.Minor}" : null; + var isMinReleased = min <= versioningSystem.Current; + var isMaxReleased = max is not null && max <= versioningSystem.Current; + + return versionSpec.Kind switch + { + // Greater than or equal (x.x+, x.x, x.x.x+, x.x.x) + VersionSpecKind.GreaterThanOrEqual => GenerateGteAvailabilityText(lifecycle, minVersion, isMinReleased, lifecycleCount), + + // ange (x.x-y.y, x.x.x-y.y.y) + VersionSpecKind.Range => GenerateRangeAvailabilityText(lifecycle, minVersion, maxVersion!, isMinReleased, isMaxReleased, lifecycleCount), + + // Exact (=x.x, =x.x.x) + VersionSpecKind.Exact => GenerateExactAvailabilityText(lifecycle, minVersion, isMinReleased, lifecycleCount), + + _ => null + }; + } + + /// + /// Generates availability text for greater-than-or-equal version type. + /// + private static string? GenerateGteAvailabilityText(ProductLifecycle lifecycle, string version, bool isReleased, int lifecycleCount) + { + if (isReleased) + { + return lifecycle switch + { + ProductLifecycle.Removed => $"Removed in {version}", + ProductLifecycle.Unavailable => $"Unavailable since {version}", + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} since {version}" + }; + } + + // Unreleased + return lifecycle switch + { + ProductLifecycle.Deprecated => "Planned for deprecation", + ProductLifecycle.Removed => "Planned for removal", + ProductLifecycle.Unavailable when lifecycleCount == 1 => "Unavailable", + ProductLifecycle.Unavailable => null, // Do not add to availability list + _ when lifecycleCount == 1 => "Planned", + _ => null // Do not add to availability list + }; + } + + /// + /// Generates availability text for range version type. + /// + private static string? GenerateRangeAvailabilityText( + ProductLifecycle lifecycle, string minVersion, string maxVersion, bool isMinReleased, bool isMaxReleased, int lifecycleCount) + { + if (isMaxReleased) + { + return lifecycle switch + { + ProductLifecycle.Removed => $"Removed in {minVersion}", + ProductLifecycle.Unavailable => $"Unavailable from {minVersion} to {maxVersion}", + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} from {minVersion} to {maxVersion}" + }; } - var disclaimer = GetDisclaimer(applicability.Lifecycle, versioningSystem.Id); - if (disclaimer is not null) - tooltipText = $"{tooltipText}\n\n{disclaimer}"; + if (isMinReleased) + { + // Max is not released, min is released -> treat as "since min" + return lifecycle switch + { + ProductLifecycle.Removed => $"Removed in {minVersion}", + ProductLifecycle.Unavailable => $"Unavailable since {minVersion}", + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} since {minVersion}" + }; + } - return tooltipText; + // Neither released + return lifecycle switch + { + ProductLifecycle.Deprecated => "Planned for deprecation", + ProductLifecycle.Removed => "Planned for removal", + ProductLifecycle.Unavailable => null, // Do not add to availability list + _ when lifecycleCount == 1 => "Planned", + _ => null // Do not add to availability list + }; } - private static string? GetDisclaimer(ProductLifecycle lifecycle, VersioningSystemId versioningSystemId) => lifecycle switch + /// + /// Generates availability text for exact version type. + /// + private static string? GenerateExactAvailabilityText(ProductLifecycle lifecycle, string version, bool isReleased, int lifecycleCount) { - 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.", - 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.", - ProductLifecycle.GenerallyAvailable => versioningSystemId is VersioningSystemId.Stack - ? "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." - : null, - _ => null - }; + if (isReleased) + { + return lifecycle switch + { + ProductLifecycle.Removed => $"Removed in {version}", + ProductLifecycle.Unavailable => $"Unavailable in {version}", + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} in {version}" + }; + } + + // Unreleased + return lifecycle switch + { + ProductLifecycle.Deprecated => "Planned for deprecation", + ProductLifecycle.Removed => "Planned for removal", + ProductLifecycle.Unavailable => null, // Do not add to availability list + _ when lifecycleCount == 1 => "Planned", + _ => null // Do not add to availability list + }; + } + + /// + /// Gets the plain product name without HTML entities for use in text substitution. + /// + private static string GetPlainProductName(string displayName) => + displayName.Replace(" ", " "); + + /// + /// Determines if a version should be considered released. + /// + private static bool IsVersionReleased(Applicability applicability, VersioningSystem versioningSystem) + { + var versionSpec = applicability.Version; + + // No version specified - consider released + if (versionSpec is null or AllVersionsSpec) + return true; + + // For ranges, check the max version + if (versionSpec.Kind == VersionSpecKind.Range && versionSpec.Max is not null) + return versionSpec.Max <= versioningSystem.Current; + + // For GTE and Exact, check the min version + return versionSpec.Min <= versioningSystem.Current; + } private static string BuildBadgeLifecycleText( Applicability applicability, @@ -253,55 +431,43 @@ private static string BuildBadgeLifecycleText( /// private static string GetBadgeVersionText(VersionSpec? versionSpec, VersioningSystem versioningSystem) { - // When no version is specified, check if we should show the base version - if (versionSpec is null) - { - return versioningSystem.Base != AllVersionsSpec.Instance.Min - ? $"{versioningSystem.Base.Major}.{versioningSystem.Base.Minor}+" - : string.Empty; // Otherwise, this is an unversioned product, show no version - } - - var kind = versionSpec.Kind; - var min = versionSpec.Min; - var max = versionSpec.Max; - - // Check if versions are released - var minReleased = min <= versioningSystem.Current; - var maxReleased = max is not null && max <= versioningSystem.Current; - - return kind switch + // When no version is specified (null or AllVersionsSpec), check if we should show the base version + switch (versionSpec) { - VersionSpecKind.GreaterThanOrEqual => minReleased - ? $"{min.Major}.{min.Minor}+" - : string.Empty, + case AllVersionsSpec: + return string.Empty; + case null: + // Only show base version if the product is versioned + return versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major + ? $"{versioningSystem.Base.Major}.{versioningSystem.Base.Minor}+" + : string.Empty; + default: + var kind = versionSpec.Kind; + var min = versionSpec.Min; + var max = versionSpec.Max; + + // Check if versions are released + var minReleased = min <= versioningSystem.Current; + var maxReleased = max is not null && max <= versioningSystem.Current; + + return kind switch + { + VersionSpecKind.GreaterThanOrEqual => minReleased + ? $"{min.Major}.{min.Minor}+" + : string.Empty, - VersionSpecKind.Range => maxReleased - ? $"{min.Major}.{min.Minor}-{max!.Major}.{max.Minor}" - : minReleased - ? $"{min.Major}.{min.Minor}+" - : string.Empty, + VersionSpecKind.Range => maxReleased + ? $"{min.Major}.{min.Minor}-{max!.Major}.{max.Minor}" + : minReleased + ? $"{min.Major}.{min.Minor}+" + : string.Empty, - VersionSpecKind.Exact => minReleased - ? $"{min.Major}.{min.Minor}" - : string.Empty, + VersionSpecKind.Exact => minReleased + ? $"{min.Major}.{min.Minor}" + : string.Empty, - _ => string.Empty - }; + _ => string.Empty + }; + } } - private static int GetLifecycleOrder(ProductLifecycle lifecycle) => lifecycle switch - { - ProductLifecycle.GenerallyAvailable => 0, - ProductLifecycle.Beta => 1, - ProductLifecycle.TechnicalPreview => 2, - ProductLifecycle.Planned => 3, - ProductLifecycle.Deprecated => 4, - ProductLifecycle.Removed => 5, - ProductLifecycle.Unavailable => 6, - _ => 999 - }; - - /// - /// Checks if a version should be considered released - /// - private static bool IsVersionReleased(SemVersion version, VersioningSystem versioningSystem) => version <= versioningSystem.Current; } diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml b/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml index 815050a7a..8dab6533e 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml @@ -2,36 +2,17 @@ @foreach (var item in Model.GetApplicabilityItems()) { - - @item.Key - - @if (!string.IsNullOrEmpty(item.Key) && (item.RenderData.ShowLifecycleName || item.RenderData.ShowVersion || !string.IsNullOrEmpty(item.RenderData.BadgeLifecycleText))) - { - - } - - @if (item.RenderData.ShowLifecycleName) - { - @item.RenderData.LifecycleName - } - @if (item.RenderData.ShowVersion) - { - - @item.RenderData.Version - - } - else - { - @item.RenderData.BadgeLifecycleText - } - @if (item.RenderData.HasMultipleLifecycles) - { - - - - - - } - - + } diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs index c2d83697c..c2ab0447a 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs @@ -2,7 +2,6 @@ // 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; @@ -63,105 +62,96 @@ public class ApplicableToViewModel public IEnumerable GetApplicabilityItems() { - var items = new List(); + var rawItems = new List(); if (AppliesTo.Serverless is not null) { - items.AddRange(AppliesTo.Serverless.AllProjects is not null - ? ProcessSingleCollection(AppliesTo.Serverless.AllProjects, ApplicabilityMappings.Serverless) - : ProcessMappedCollections(AppliesTo.Serverless, ServerlessMappings)); + rawItems.AddRange(AppliesTo.Serverless.AllProjects is not null + ? CollectFromCollection(AppliesTo.Serverless.AllProjects, ApplicabilityMappings.Serverless) + : CollectFromMappings(AppliesTo.Serverless, ServerlessMappings)); } if (AppliesTo.Stack is not null) - items.AddRange(ProcessSingleCollection(AppliesTo.Stack, ApplicabilityMappings.Stack)); + rawItems.AddRange(CollectFromCollection(AppliesTo.Stack, ApplicabilityMappings.Stack)); if (AppliesTo.Deployment is not null) - items.AddRange(ProcessMappedCollections(AppliesTo.Deployment, DeploymentMappings)); + rawItems.AddRange(CollectFromMappings(AppliesTo.Deployment, DeploymentMappings)); if (AppliesTo.ProductApplicability is not null) - items.AddRange(ProcessMappedCollections(AppliesTo.ProductApplicability, ProductMappings)); + rawItems.AddRange(CollectFromMappings(AppliesTo.ProductApplicability, ProductMappings)); if (AppliesTo.Product is not null) - items.AddRange(ProcessSingleCollection(AppliesTo.Product, ApplicabilityMappings.Product)); + rawItems.AddRange(CollectFromCollection(AppliesTo.Product, ApplicabilityMappings.Product)); - return CombineItemsByKey(items); + return RenderGroupedItems(rawItems); } - private IEnumerable ProcessSingleCollection(AppliesCollection collection, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition) - { - var versioningSystem = VersionsConfig.GetVersioningSystem(applicabilityDefinition.VersioningSystemId); - return ProcessApplicabilityCollection(collection, applicabilityDefinition, versioningSystem); - } + /// + /// Collects raw applicability items from a single collection. + /// + private static IEnumerable CollectFromCollection( + AppliesCollection collection, + ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition) => + collection.Select(applicability => new RawApplicabilityItem( + Key: applicabilityDefinition.Key, + Applicability: applicability, + ApplicabilityDefinition: applicabilityDefinition + )); /// - /// Uses mapping dictionary to eliminate repetitive code when processing multiple collections + /// Collects raw applicability items from mapped collections. /// - private IEnumerable ProcessMappedCollections(T source, Dictionary, ApplicabilityMappings.ApplicabilityDefinition> mappings) + private static IEnumerable CollectFromMappings( + T source, + Dictionary, ApplicabilityMappings.ApplicabilityDefinition> mappings) { - var items = new List(); + var items = new List(); foreach (var (propertySelector, applicabilityDefinition) in mappings) { var collection = propertySelector(source); if (collection is not null) - items.AddRange(ProcessSingleCollection(collection, applicabilityDefinition)); + items.AddRange(CollectFromCollection(collection, applicabilityDefinition)); } return items; } - private IEnumerable ProcessApplicabilityCollection( - AppliesCollection applications, - ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, - VersioningSystem versioningSystem) => - applications.Select(applicability => - { - var renderData = _applicabilityRenderer.RenderApplicability( - applicability, - applicabilityDefinition, - versioningSystem, - applications); - - return new ApplicabilityItem( - Key: applicabilityDefinition.Key, - PrimaryApplicability: applicability, - RenderData: renderData, - ApplicabilityDefinition: applicabilityDefinition - ); - }); - /// - /// Combines multiple applicability items with the same key into a single item with combined tooltip + /// Groups raw items by key and renders each group using the unified renderer. /// - private IEnumerable CombineItemsByKey(List items) => items + private IEnumerable RenderGroupedItems(List rawItems) => + rawItems .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 items = group.ToList(); + var applicabilityDefinition = items.First().ApplicabilityDefinition; var versioningSystem = VersionsConfig.GetVersioningSystem(applicabilityDefinition.VersioningSystemId); + var allApplicabilities = items.Select(i => i.Applicability).ToList(); - var combinedRenderData = _applicabilityRenderer.RenderCombinedApplicability( + var renderData = _applicabilityRenderer.RenderApplicability( allApplicabilities, applicabilityDefinition, - versioningSystem, - new AppliesCollection(allApplicabilities.ToArray())); + versioningSystem); // Select the closest version to current as the primary display var primaryApplicability = ApplicabilitySelector.GetPrimaryApplicability(allApplicabilities, versioningSystem.Current); return new ApplicabilityItem( - Key: firstItem.Key, + Key: items.First().Key, PrimaryApplicability: primaryApplicability, - RenderData: combinedRenderData, + RenderData: renderData, ApplicabilityDefinition: applicabilityDefinition ); }); - - + /// + /// Intermediate representation before rendering. + /// + private sealed record RawApplicabilityItem( + string Key, + Applicability Applicability, + ApplicabilityMappings.ApplicabilityDefinition ApplicabilityDefinition + ); } diff --git a/src/Elastic.Markdown/Myst/Components/LifecycleDescriptions.cs b/src/Elastic.Markdown/Myst/Components/LifecycleDescriptions.cs new file mode 100644 index 000000000..5a37ced3f --- /dev/null +++ b/src/Elastic.Markdown/Myst/Components/LifecycleDescriptions.cs @@ -0,0 +1,74 @@ +// 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 Elastic.Documentation.AppliesTo; + +namespace Elastic.Markdown.Myst.Components; + +/// +/// Contains static lifecycle descriptions for use in applicability popovers. +/// +public static class LifecycleDescriptions +{ + /// + /// Gets the lifecycle description for a given lifecycle and release state. + /// The returned text may contain a {product} placeholder that should be replaced with the product name. + /// + /// The product lifecycle state. + /// Whether the version is released. + /// The description text, or null if not applicable. + private static string? GetDescription(ProductLifecycle lifecycle, bool isReleased) => + Descriptions.GetValueOrDefault((lifecycle, isReleased)); + + /// + /// Gets the lifecycle description with the product name substituted. + /// + /// The product lifecycle state. + /// Whether the version is released. + /// The product name to substitute for {product}. + /// The description text with product name substituted, or null if not applicable. + public static string? GetDescriptionWithProduct(ProductLifecycle lifecycle, bool isReleased, string productName) + { + var description = GetDescription(lifecycle, isReleased); + return description?.Replace("{product}", productName); + } + + private static readonly Dictionary<(ProductLifecycle Lifecycle, bool IsReleased), string> Descriptions = new() + { + // Preview + [(ProductLifecycle.TechnicalPreview, true)] = + "This functionality is in technical preview and is not ready for production usage. Technical preview features may change or be removed at any time. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. Specific Support terms apply.", + [(ProductLifecycle.TechnicalPreview, false)] = + "We plan to add this functionality in a future {product} update. Subject to changes.", + + // Beta + [(ProductLifecycle.Beta, true)] = + "This functionality is in beta and is not ready for production usage. For beta features, 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. Specific Support terms apply.", + [(ProductLifecycle.Beta, false)] = + "We plan to add this functionality in a future {product} update. Subject to changes.", + + // GA + [(ProductLifecycle.GenerallyAvailable, true)] = + "This functionality is generally available and ready for production usage.", + [(ProductLifecycle.GenerallyAvailable, false)] = + "We plan to add this functionality in a future {product} update. Subject to changes.", + + // Deprecated + [(ProductLifecycle.Deprecated, true)] = + "This functionality is deprecated. You can still use it, but it'll be removed in a future {product} update.", + [(ProductLifecycle.Deprecated, false)] = + "This functionality is planned to be deprecated in a future {product} update. Subject to changes.", + + // Removed + [(ProductLifecycle.Removed, true)] = + "This functionality was removed. You can no longer use it if you're running on this version or a later one.", + [(ProductLifecycle.Removed, false)] = + "This functionality is planned to be removed in an upcoming {product} update. Subject to changes.", + + // Unavailable + [(ProductLifecycle.Unavailable, true)] = + "{product} doesn't include this functionality." + }; +} + diff --git a/src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs b/src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs new file mode 100644 index 000000000..736233d6f --- /dev/null +++ b/src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs @@ -0,0 +1,192 @@ +// 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 Elastic.Documentation.Configuration.Versions; + +namespace Elastic.Markdown.Myst.Components; + +/// +/// Contains static product descriptions for use in applicability popovers. +/// +public static class ProductDescriptions +{ + /// + /// Product information record containing description, additional info, and version note flag. + /// + /// The product description shown at the top of the popover (required). + /// Additional availability information shown near the bottom of the popover (optional). + /// Whether to include the version note at the bottom of the popover. + public record ProductInfo( + string Description, + string? AdditionalAvailabilityInfo, + bool IncludeVersionNote + ); + + /// + /// The version note text shown at the bottom of versioned product popovers. + /// + public const string VersionNote = + "This documentation corresponds to the latest patch available for each minor version. If you're not using the latest patch, check the release notes for changes."; + + public static ProductInfo? GetProductInfo(VersioningSystemId versioningSystemId) => + Descriptions.GetValueOrDefault(versioningSystemId); + + private static readonly Dictionary Descriptions = new() + { + // Stack + [VersioningSystemId.Stack] = new ProductInfo( + Description: "The Elastic Stack includes Elastic's core products such as Elasticsearch, Kibana, Logstash, and Beats.", + AdditionalAvailabilityInfo: "Unless stated otherwise on the page, this functionality is available when your Elastic Stack is deployed on Elastic Cloud Hosted, Elastic Cloud Enterprise, Elastic Cloud on Kubernetes, and self-managed environments.", + IncludeVersionNote: true + ), + + // Serverless + [VersioningSystemId.Serverless] = new ProductInfo( + Description: "Elastic Cloud Serverless projects are autoscaled environments, fully managed by Elastic and available on Elastic Cloud.", + AdditionalAvailabilityInfo: "Serverless interfaces and procedures might differ from classic Elastic Stack deployments.", + IncludeVersionNote: false + ), + + // Serverless Project Types + [VersioningSystemId.ElasticsearchProject] = new ProductInfo( + Description: "Elastic Cloud Serverless projects are autoscaled environments, fully managed by Elastic and available on Elastic Cloud.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: false + ), + [VersioningSystemId.ObservabilityProject] = new ProductInfo( + Description: "Elastic Cloud Serverless projects are autoscaled environments, fully managed by Elastic and available on Elastic Cloud.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: false + ), + [VersioningSystemId.SecurityProject] = new ProductInfo( + Description: "Elastic Cloud Serverless projects are autoscaled environments, fully managed by Elastic and available on Elastic Cloud.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: false + ), + + // Deployment Types + [VersioningSystemId.Ess] = new ProductInfo( + Description: "Elastic Cloud Hosted is a deployment of the Elastic Stack that's hosted on Elastic Cloud.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: false + ), + [VersioningSystemId.Ece] = new ProductInfo( + Description: "Elastic Cloud Enterprise is a self-managed orchestration platform for deploying and managing the Elastic Stack at scale.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.Eck] = new ProductInfo( + Description: "Elastic Cloud on Kubernetes extends Kubernetes orchestration capabilities to allow you to deploy and manage components of the Elastic Stack.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.Self] = new ProductInfo( + Description: "Self-managed deployments are Elastic Stack deployments managed without the assistance of an orchestrator.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + + // Products + [VersioningSystemId.Ecctl] = new ProductInfo( + Description: "ECCTL is the command line interface for the Elastic Cloud and Elastic Cloud Enterprise APIs.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.Curator] = new ProductInfo( + Description: "Curator is a tool that helps you to manage your Elasticsearch indices and snapshots to save space and improve performance.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + + // APM Agents + [VersioningSystemId.ApmAgentDotnet] = new ProductInfo( + Description: "The Elastic APM .NET agent enables you to trace the execution of operations in your .NET applications, sending performance metrics and errors to the Elastic APM server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentGo] = new ProductInfo( + Description: "The Elastic APM Go agent enables you to trace the execution of operations in your Go applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentJava] = new ProductInfo( + Description: "The Elastic APM Java agent enables you to trace the execution of operations in your Java applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentNode] = new ProductInfo( + Description: "The Elastic APM Node.js agent enables you to trace the execution of operations in your Node.js applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentPhp] = new ProductInfo( + Description: "The Elastic APM PHP agent enables you to trace the execution of operations in your PHP applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentPython] = new ProductInfo( + Description: "The Elastic APM Python agent enables you to trace the execution of operations in your Python applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentRuby] = new ProductInfo( + Description: "The Elastic APM Ruby agent enables you to trace the execution of operations in your Ruby applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentRumJs] = new ProductInfo( + Description: "The Elastic APM RUM JavaScript agent enables you to trace the execution of operations in your web applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + + // EDOT Products + [VersioningSystemId.EdotCollector] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) Collector retrieves traces, metrics, and logs from your infrastructure and applications, and forwards them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotIos] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) iOS SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotAndroid] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) Android SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotDotnet] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) .NET SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotJava] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) Java SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotNode] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) Node.js SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotPhp] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) PHP SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotPython] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) Python SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotCfAws] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) Cloud Forwarder allows you to collect and send your telemetry data to Elastic Observability from AWS, GCP, and Azure.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + }; +} + From 8db266943eaa84b87d77603250b9d9cc09f44db5 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Fri, 12 Dec 2025 12:56:16 -0300 Subject: [PATCH 19/61] Preview: send a limited assembler build to the preview environment --- .github/workflows/preview-build.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 19c60bdb7..7c15900b9 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -510,6 +510,30 @@ jobs: environment_url: `https://docs-v3-preview.elastic.dev${process.env.LANDING_PAGE_PATH}`, log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, }) + + - name: Build assembled documentation (temporary) + id: internal-assembler-build + if: > + github.repository == 'elastic/docs-builder' + && github.head_ref == 'applies-to-popovers' + && steps.deployment.outputs.result + run: | + dotnet run --project src/tooling/docs-builder -- assemble --skip-private-repositories + + - name: Upload assembled docs to S3 (temporary) + if: > + github.repository == 'elastic/docs-builder' + && steps.internal-assembler-build.outcome == 'success' + env: + AWS_RETRY_MODE: standard + AWS_MAX_ATTEMPTS: 6 + run: | + aws s3 sync .artifacts/assembly/docs "s3://elastic-docs-v3-website-preview/assembled-docs/" --delete --no-follow-symlinks + aws cloudfront create-invalidation \ + --distribution-id EKT7LT5PM8RKS \ + --paths "/internal-preview" "/internal-preview/*" + + comment: if: > startsWith(github.event_name, 'pull_request') From 817cfc64821659c6abb0e8d8248dadcfd872fdf5 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Fri, 12 Dec 2025 13:00:33 -0300 Subject: [PATCH 20/61] Fix Call to the Renderer --- src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs index c2ab0447a..779c2a028 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs @@ -130,7 +130,7 @@ private IEnumerable RenderGroupedItems(List i.Applicability).ToList(); - var renderData = _applicabilityRenderer.RenderApplicability( + var renderData = ApplicabilityRenderer.RenderApplicability( allApplicabilities, applicabilityDefinition, versioningSystem); From 0f2ce56e187b4ac74ea6182fadec6806a1ad5766 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Fri, 12 Dec 2025 13:01:46 -0300 Subject: [PATCH 21/61] Remove unused var --- src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs index 779c2a028..cfbe91fce 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs @@ -9,8 +9,6 @@ namespace Elastic.Markdown.Myst.Components; public class ApplicableToViewModel { - private readonly ApplicabilityRenderer _applicabilityRenderer = new(); - public required bool Inline { get; init; } public bool ShowTooltip { get; init; } = true; From 917d18b193006e66d1370eb255bc63f97f8fd103 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Fri, 12 Dec 2025 13:07:59 -0300 Subject: [PATCH 22/61] typo --- .github/workflows/preview-build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index e5a264c3d..0897c2acd 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -521,7 +521,6 @@ jobs: id: internal-assembler-build if: > github.repository == 'elastic/docs-builder' - && github.head_ref == 'applies-to-popovers' && steps.deployment.outputs.result run: | dotnet run --project src/tooling/docs-builder -- assemble --skip-private-repositories From 6a1fde81924d9e56a5927330e791d001c045f54c Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Fri, 12 Dec 2025 13:09:39 -0300 Subject: [PATCH 23/61] Invalidate correct subfolder --- .github/workflows/preview-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 0897c2acd..637ad5969 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -536,7 +536,7 @@ jobs: aws s3 sync .artifacts/assembly/docs "s3://elastic-docs-v3-website-preview/assembled-docs/" --delete --no-follow-symlinks aws cloudfront create-invalidation \ --distribution-id EKT7LT5PM8RKS \ - --paths "/internal-preview" "/internal-preview/*" + --paths "/assembled-docs" "/assembled-docs/*" comment: From 0458208e48ca99c6cc562ac9d786e28def45ecfd Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Fri, 12 Dec 2025 13:25:09 -0300 Subject: [PATCH 24/61] Fix policy path --- .github/workflows/preview-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 637ad5969..3a4639c59 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -533,7 +533,7 @@ jobs: AWS_RETRY_MODE: standard AWS_MAX_ATTEMPTS: 6 run: | - aws s3 sync .artifacts/assembly/docs "s3://elastic-docs-v3-website-preview/assembled-docs/" --delete --no-follow-symlinks + aws s3 sync .artifacts/assembly/docs "s3://elastic-docs-v3-website-preview${PATH_PREFIX}/assembled-docs/" --delete --no-follow-symlinks aws cloudfront create-invalidation \ --distribution-id EKT7LT5PM8RKS \ --paths "/assembled-docs" "/assembled-docs/*" From cd331f0f772ede0f0c150347b8655348f47dc023 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Fri, 12 Dec 2025 13:26:08 -0300 Subject: [PATCH 25/61] Fix policy --- .github/workflows/preview-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 3a4639c59..cc7cada92 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -536,7 +536,7 @@ jobs: aws s3 sync .artifacts/assembly/docs "s3://elastic-docs-v3-website-preview${PATH_PREFIX}/assembled-docs/" --delete --no-follow-symlinks aws cloudfront create-invalidation \ --distribution-id EKT7LT5PM8RKS \ - --paths "/assembled-docs" "/assembled-docs/*" + --paths "${PATH_PREFIX}/assembled-docs" "${PATH_PREFIX}/assembled-docs/*" comment: From d728b6a53a271509304aed3318d180368e2154c1 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Fri, 12 Dec 2025 13:40:56 -0300 Subject: [PATCH 26/61] Send path-prefix to assembler. --- .github/workflows/preview-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index cc7cada92..56fe2d272 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -523,7 +523,7 @@ jobs: github.repository == 'elastic/docs-builder' && steps.deployment.outputs.result run: | - dotnet run --project src/tooling/docs-builder -- assemble --skip-private-repositories + dotnet run --project src/tooling/docs-builder -- assemble --skip-private-repositories --path-prefix "${PATH_PREFIX}/assembled-docs" - name: Upload assembled docs to S3 (temporary) if: > From b799f697ba4caa7d2ca70ceaaf68060feffeaf17 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Fri, 12 Dec 2025 13:54:20 -0300 Subject: [PATCH 27/61] Revert temporary assembler build for now --- .github/workflows/preview-build.yml | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 56fe2d272..837cec41c 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -516,29 +516,6 @@ jobs: environment_url: `https://docs-v3-preview.elastic.dev${process.env.LANDING_PAGE_PATH}`, log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, }) - - - name: Build assembled documentation (temporary) - id: internal-assembler-build - if: > - github.repository == 'elastic/docs-builder' - && steps.deployment.outputs.result - run: | - dotnet run --project src/tooling/docs-builder -- assemble --skip-private-repositories --path-prefix "${PATH_PREFIX}/assembled-docs" - - - name: Upload assembled docs to S3 (temporary) - if: > - github.repository == 'elastic/docs-builder' - && steps.internal-assembler-build.outcome == 'success' - env: - AWS_RETRY_MODE: standard - AWS_MAX_ATTEMPTS: 6 - run: | - aws s3 sync .artifacts/assembly/docs "s3://elastic-docs-v3-website-preview${PATH_PREFIX}/assembled-docs/" --delete --no-follow-symlinks - aws cloudfront create-invalidation \ - --distribution-id EKT7LT5PM8RKS \ - --paths "${PATH_PREFIX}/assembled-docs" "${PATH_PREFIX}/assembled-docs/*" - - comment: if: > startsWith(github.event_name, 'pull_request') From a013f99bf134987d211654eca299fa581a6612e4 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Sun, 14 Dec 2025 22:00:24 -0300 Subject: [PATCH 28/61] Fix admonition tests --- .../Directives/AdmonitionTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs b/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs index 49d427b2b..5af9d58af 100644 --- a/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs @@ -267,7 +267,7 @@ public void RendersAppliesToInHtml() var html = Html; html.Should().Contain("applies applies-admonition"); html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); + html.Should().Contain("applies-to-popover"); } } @@ -299,7 +299,7 @@ public void RendersAppliesToInHtml() var html = Html; html.Should().Contain("applies applies-admonition"); html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); + html.Should().Contain("applies-to-popover"); } } @@ -331,7 +331,7 @@ public void RendersAppliesToInHtml() var html = Html; html.Should().Contain("applies applies-admonition"); html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); + html.Should().Contain("applies-to-popover"); } } @@ -363,7 +363,7 @@ public void RendersAppliesToInHtml() var html = Html; html.Should().Contain("applies applies-admonition"); html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); + html.Should().Contain("applies-to-popover"); } } @@ -395,6 +395,6 @@ public void RendersAppliesToInHtml() var html = Html; html.Should().Contain("applies applies-admonition"); html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); + html.Should().Contain("applies-to-popover"); } } From 2675bafd984786369a5b5ebd52acf75214ed23d0 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 03:31:30 -0300 Subject: [PATCH 29/61] Transfer applicablity popover data properly as JSON --- .../web-components/AppliesToPopover.tsx | 374 +++++++++++------- .../Myst/Components/ApplicabilityRenderer.cs | 107 +++-- .../Components/ApplicableToComponent.cshtml | 4 +- src/Elastic.Markdown/Page/IndexViewModel.cs | 2 + 4 files changed, 279 insertions(+), 208 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx index 09c3ab437..23380b2d7 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx @@ -7,6 +7,19 @@ import r2wc from '@r2wc/react-to-web-component' import * as React from 'react' import { useState, useRef, useEffect, useCallback } from 'react' +type PopoverAvailabilityItem = { + text: string + lifecycleDescription?: string +} + +type PopoverData = { + productDescription?: string + availabilityItems: PopoverAvailabilityItem[] + additionalInfo?: string + showVersionNote: boolean + versionNote?: string +} + type AppliesToPopoverProps = { badgeKey?: string badgeLifecycleText?: string @@ -16,7 +29,7 @@ type AppliesToPopoverProps = { showLifecycleName?: boolean showVersion?: boolean hasMultipleLifecycles?: boolean - popoverContent?: string + popoverData?: PopoverData showPopover?: boolean isInline?: boolean } @@ -30,22 +43,30 @@ const AppliesToPopover = ({ showLifecycleName, showVersion, hasMultipleLifecycles, - popoverContent, + popoverData, showPopover = true, isInline = false, }: AppliesToPopoverProps) => { const [isOpen, setIsOpen] = useState(false) const [isPinned, setIsPinned] = useState(false) + const [openItems, setOpenItems] = useState>(new Set()) const popoverId = useGeneratedHtmlId({ prefix: 'appliesToPopover' }) const contentRef = useRef(null) const badgeRef = useRef(null) const hoverTimeoutRef = useRef | null>(null) + const hasPopoverContent = popoverData && ( + popoverData.productDescription || + popoverData.availabilityItems.length > 0 || + popoverData.additionalInfo || + popoverData.showVersionNote + ) + const openPopover = useCallback(() => { - if (showPopover && popoverContent) { + if (showPopover && hasPopoverContent) { setIsOpen(true) } - }, [showPopover, popoverContent]) + }, [showPopover, hasPopoverContent]) const closePopover = useCallback(() => { if (!isPinned) { @@ -54,7 +75,7 @@ const AppliesToPopover = ({ }, [isPinned]) const handleClick = useCallback(() => { - if (showPopover && popoverContent) { + if (showPopover && hasPopoverContent) { if (isPinned) { // If already pinned, unpin and close setIsPinned(false) @@ -65,7 +86,20 @@ const AppliesToPopover = ({ setIsOpen(true) } } - }, [showPopover, popoverContent, isPinned]) + }, [showPopover, hasPopoverContent, isPinned]) + + const toggleItem = useCallback((index: number, e: React.MouseEvent) => { + e.stopPropagation() + setOpenItems((prev) => { + const next = new Set(prev) + if (next.has(index)) { + next.delete(index) + } else { + next.add(index) + } + return next + }) + }, []) const handleClosePopover = useCallback(() => { setIsPinned(false) @@ -121,17 +155,12 @@ const AppliesToPopover = ({ } }, [isOpen]) - // Handle details/summary elements for collapsible sections + // Reset open items when popover closes useEffect(() => { - if (!contentRef.current) return - - const details = contentRef.current.querySelectorAll('details') - details.forEach((detail) => { - detail.addEventListener('toggle', (e) => { - e.stopPropagation() - }) - }) - }, [isOpen, popoverContent]) + if (!isOpen) { + setOpenItems(new Set()) + } + }, [isOpen]) const showSeparator = badgeKey && (showLifecycleName || showVersion || badgeLifecycleText) @@ -139,16 +168,16 @@ const AppliesToPopover = ({ const badgeButton = ( { if ( showPopover && - popoverContent && + hasPopoverContent && (e.key === 'Enter' || e.key === ' ') ) { e.preventDefault() @@ -190,10 +219,91 @@ const AppliesToPopover = ({ ) - if (!showPopover || !popoverContent) { + if (!showPopover || !hasPopoverContent) { return badgeButton } + const renderAvailabilityItem = ( + item: PopoverAvailabilityItem, + index: number + ) => { + const isItemOpen = openItems.has(index) + + if (item.lifecycleDescription) { + return ( +
+
toggleItem(index, e)} + css={css` + display: flex; + align-items: center; + cursor: pointer; + padding: 4px 0; + color: var(--color-blue-elastic, #0077cc); + font-weight: 500; + + &::before { + content: ''; + display: inline-block; + width: 6px; + height: 6px; + border-right: 2px solid currentColor; + border-bottom: 2px solid currentColor; + transform: ${isItemOpen + ? 'rotate(45deg)' + : 'rotate(-45deg)'}; + margin-right: 8px; + transition: transform 0.15s ease; + } + + &:hover { + color: var(--color-blue-hover, #005fa3); + } + `} + > + {item.text} +
+ {isItemOpen && ( +

+ {item.lifecycleDescription} +

+ )} +
+ ) + } + + // Simple item without collapsible content + return ( +

+ {item.text} +

+ ) + } + return (
+ {/* Product description */} + {popoverData?.productDescription && ( +

+ )} - /* Product description */ - .popover-product-description { - margin: 0 0 16px 0; - color: var(--color-grey-80, #343741); - - strong { - display: inline; - font-weight: 700; - color: var(--color-grey-100, #1a1c21); - } - } - - /* Availability section */ - .popover-availability-title { - margin: 0 0 4px 0; - - strong { - display: inline; - font-weight: 700; - font-size: 15px; - color: var(--color-grey-100, #1a1c21); - } - } - - .popover-availability-intro { - margin: 0 0 8px 0; - color: var(--color-grey-70, #535966); - font-size: 13px; - } - - /* Availability items (collapsible details/summary) */ - .popover-availability-item { - margin: 0 0 4px 0; - border: none; - background: none; - - &[open] .popover-availability-summary::before { - transform: rotate(45deg); - } - } - - .popover-availability-summary { - display: flex; - align-items: center; - cursor: pointer; - list-style: none; - padding: 4px 0; - color: var(--color-blue-elastic, #0077cc); - font-weight: 500; - - &::-webkit-details-marker { - display: none; - } - - &::before { - content: ''; - display: inline-block; - width: 6px; - height: 6px; - border-right: 2px solid currentColor; - border-bottom: 2px solid currentColor; - transform: rotate(-45deg); - margin-right: 8px; - transition: transform 0.15s ease; - } - - &:hover { - color: var(--color-blue-hover, #005fa3); - } - } - - .popover-availability-text { - flex: 1; - } - - .popover-lifecycle-description { - margin: 4px 0 8px 16px; - padding: 8px 12px; - background: var(--color-grey-5, #f5f7fa); - border-radius: 4px; - font-size: 13px; - color: var(--color-grey-80, #343741); - line-height: 1.5; - } - - /* Simple availability item (no collapsible content) */ - .popover-availability-item-simple { - margin: 0 0 4px 0; - padding: 4px 0 4px 16px; - color: var(--color-grey-80, #343741); - } - - /* Additional availability info */ - .popover-additional-info { - margin: 12px 0 0 0; - padding-top: 12px; - border-top: 1px solid var(--color-grey-15, #e0e5ee); - color: var(--color-grey-70, #535966); - font-size: 13px; - } + {/* Availability section */} + {popoverData && popoverData.availabilityItems.length > 0 && ( + <> +

+ + Availability + +

+

+ The functionality described here is: +

+ {popoverData.availabilityItems.map((item, index) => + renderAvailabilityItem(item, index) + )} + + )} - /* Version note */ - .popover-version-note { - display: flex; - align-items: flex-start; - gap: 8px; - margin: 12px 0 0 0; - padding: 8px 12px; - background: var(--color-grey-5, #f5f7fa); - border-radius: 4px; - font-size: 12px; - color: var(--color-grey-70, #535966); - line-height: 1.5; - } + {/* Additional availability info */} + {popoverData?.additionalInfo && ( +

+ {popoverData.additionalInfo} +

+ )} - .popover-note-icon { - flex-shrink: 0; - color: var(--color-blue-elastic, #0077cc); - font-size: 14px; - } - `} - dangerouslySetInnerHTML={{ __html: popoverContent }} - /> + {/* Version note */} + {popoverData?.showVersionNote && popoverData?.versionNote && ( +

+ + ⓘ + + {popoverData.versionNote} +

+ )} +
) } @@ -377,7 +461,7 @@ customElements.define( showLifecycleName: 'boolean', showVersion: 'boolean', hasMultipleLifecycles: 'boolean', - popoverContent: 'string', + popoverData: 'json', showPopover: 'boolean', isInline: 'boolean', }, diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 2f5c621e7..e775e6fb1 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -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 System.Text.Json.Serialization; using Elastic.Documentation; using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Configuration.Versions; @@ -10,10 +11,29 @@ namespace Elastic.Markdown.Myst.Components; public class ApplicabilityRenderer { + /// + /// Represents a single availability item in the popover (e.g., "Generally available since 9.1"). + /// + public record PopoverAvailabilityItem( + [property: JsonPropertyName("text")] string Text, + [property: JsonPropertyName("lifecycleDescription")] string? LifecycleDescription + ); + + /// + /// Structured data for the popover content, to be serialized as JSON and rendered by the frontend. + /// + public record PopoverData( + [property: JsonPropertyName("productDescription")] string? ProductDescription, + [property: JsonPropertyName("availabilityItems")] PopoverAvailabilityItem[] AvailabilityItems, + [property: JsonPropertyName("additionalInfo")] string? AdditionalInfo, + [property: JsonPropertyName("showVersionNote")] bool ShowVersionNote, + [property: JsonPropertyName("versionNote")] string? VersionNote + ); + public record ApplicabilityRenderData( string BadgeLifecycleText, string Version, - string TooltipText, + PopoverData? PopoverData, string LifecycleClass, string LifecycleName, bool ShowLifecycleName, @@ -50,7 +70,7 @@ public static ApplicabilityRenderData RenderApplicability( ? primaryBadgeData : GetBadgeData(applicabilityToDisplay, versioningSystem, allApplications); - var popoverContent = BuildPopoverContent(applicabilityList, applicabilityDefinition, versioningSystem); + var popoverData = BuildPopoverData(applicabilityList, applicabilityDefinition, versioningSystem); // Check if there are multiple different lifecycles var hasMultipleLifecycles = applicabilityList.Select(a => a.Lifecycle).Distinct().Count() > 1; @@ -58,7 +78,7 @@ public static ApplicabilityRenderData RenderApplicability( return new ApplicabilityRenderData( BadgeLifecycleText: badgeData.BadgeLifecycleText, Version: badgeData.Version, - TooltipText: popoverContent, + PopoverData: popoverData, LifecycleClass: badgeData.LifecycleClass, LifecycleName: badgeData.LifecycleName, ShowLifecycleName: badgeData.ShowLifecycleName || (string.IsNullOrEmpty(badgeData.BadgeLifecycleText) && hasMultipleLifecycles), @@ -110,7 +130,7 @@ private sealed record BadgeData( bool ShowVersion ); - private static string BuildPopoverContent( + private static PopoverData BuildPopoverData( List applicabilities, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, VersioningSystem versioningSystem) @@ -118,15 +138,6 @@ private static string BuildPopoverContent( var productInfo = ProductDescriptions.GetProductInfo(versioningSystem.Id); var productName = GetPlainProductName(applicabilityDefinition.DisplayName); - var parts = new List(); - - // Product description - if (productInfo is not null && !string.IsNullOrEmpty(productInfo.Description)) - { - // language=html - parts.Add($"

{productInfo.Description}

"); - } - // Availability section - collect items from all applicabilities // Order by: available first (by version desc), then future (by version asc) var orderedApplicabilities = applicabilities @@ -136,53 +147,40 @@ private static string BuildPopoverContent( .ThenBy(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) .ToList(); - var allAvailabilityItems = new List(); + var availabilityItems = new List(); foreach (var applicability in orderedApplicabilities) { - var items = BuildAvailabilityItems(applicability, versioningSystem, productName, applicabilities.Count); - allAvailabilityItems.AddRange(items); + var item = BuildAvailabilityItem(applicability, versioningSystem, productName, applicabilities.Count); + if (item is not null) + availabilityItems.Add(item); } - if (allAvailabilityItems.Count > 0) - { - // language=html - parts.Add("

Availability

"); - parts.Add("

The functionality described here is:

"); - parts.Add(string.Join("\n", allAvailabilityItems)); - } + var showVersionNote = productInfo is { IncludeVersionNote: true } && + versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major; - // Additional availability info - if (productInfo is { AdditionalAvailabilityInfo: not null }) - { - // language=html - parts.Add($"

{productInfo.AdditionalAvailabilityInfo}

"); - } - - // Version note - if (productInfo is { IncludeVersionNote: true } && versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major) - { - // language=html - parts.Add($"

{ProductDescriptions.VersionNote}

"); - } - - return string.Join("\n", parts); + return new PopoverData( + ProductDescription: productInfo?.Description, + AvailabilityItems: availabilityItems.ToArray(), + AdditionalInfo: productInfo?.AdditionalAvailabilityInfo, + ShowVersionNote: showVersionNote, + VersionNote: showVersionNote ? ProductDescriptions.VersionNote : null + ); } /// - /// Builds the availability list items (details/summary elements) for an applicability. + /// Builds an availability item for an applicability. + /// Returns null if the item should not be added to the availability list. /// - private static List BuildAvailabilityItems( + private static PopoverAvailabilityItem? BuildAvailabilityItem( Applicability applicability, VersioningSystem versioningSystem, string productName, int lifecycleCount) { - var items = new List(); - var availabilityText = GenerateAvailabilityText(applicability, versioningSystem, lifecycleCount); if (availabilityText is null) - return items; + return null; var isReleased = IsVersionReleased(applicability, versioningSystem); var lifecycleDescription = LifecycleDescriptions.GetDescriptionWithProduct( @@ -191,25 +189,10 @@ private static List BuildAvailabilityItems( productName ); - // Build the details/summary element for collapsible lifecycle description - if (!string.IsNullOrEmpty(lifecycleDescription)) - { - // language=html - items.Add($""" -
-{availabilityText} -

{lifecycleDescription}

-
-"""); - } - else - { - // No collapsible content, just show the text - // language=html - items.Add($"

{availabilityText}

"); - } - - return items; + return new PopoverAvailabilityItem( + Text: availabilityText, + LifecycleDescription: lifecycleDescription + ); } /// diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml b/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml index 8dab6533e..f98c89dc0 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml @@ -1,3 +1,5 @@ +@using System.Text.Json +@using Elastic.Markdown.Page @inherits RazorSlice @foreach (var item in Model.GetApplicabilityItems()) @@ -11,7 +13,7 @@ show-lifecycle-name="@item.RenderData.ShowLifecycleName.ToString().ToLowerInvariant()" show-version="@item.RenderData.ShowVersion.ToString().ToLowerInvariant()" has-multiple-lifecycles="@item.RenderData.HasMultipleLifecycles.ToString().ToLowerInvariant()" - popover-content="@(new HtmlString(System.Web.HttpUtility.HtmlAttributeEncode(Model.ShowTooltip ? item.RenderData.TooltipText : string.Empty)))" + popover-data='@(Model.ShowTooltip && item.RenderData.PopoverData is not null ? new HtmlString(JsonSerializer.Serialize(item.RenderData.PopoverData, ViewModelSerializerContext.Default.PopoverData)) : new HtmlString(""))' show-popover="@Model.ShowTooltip.ToString().ToLowerInvariant()" is-inline="@Model.Inline.ToString().ToLowerInvariant()" > diff --git a/src/Elastic.Markdown/Page/IndexViewModel.cs b/src/Elastic.Markdown/Page/IndexViewModel.cs index 6915f8d17..d04e6f889 100644 --- a/src/Elastic.Markdown/Page/IndexViewModel.cs +++ b/src/Elastic.Markdown/Page/IndexViewModel.cs @@ -12,6 +12,7 @@ using Elastic.Documentation.Navigation; using Elastic.Documentation.Site.FileProviders; using Elastic.Markdown.IO; +using Elastic.Markdown.Myst.Components; namespace Elastic.Markdown.Page; @@ -136,4 +137,5 @@ private static Dictionary> GroupByMajorVersion(LegacyPageMa } [JsonSerializable(typeof(VersionDropDownItemViewModel[]))] +[JsonSerializable(typeof(ApplicabilityRenderer.PopoverData))] public partial class ViewModelSerializerContext : JsonSerializerContext; From 004ed8ec43e8b85339b90792426840560931b067 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 04:05:41 -0300 Subject: [PATCH 30/61] Avoid duplicate warnings --- .../AppliesTo/ApplicableToYamlConverter.cs | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs index d86e06541..211bbfdc4 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs @@ -271,15 +271,16 @@ private static void ValidateApplicabilityCollection(string key, AppliesCollectio // Rule: Only one version declaration per lifecycle var lifecycleGroups = items.GroupBy(a => a.Lifecycle).ToList(); - foreach (var group in lifecycleGroups) + var lifecyclesWithMultipleVersions = lifecycleGroups + .Where(group => group.Count(a => a.Version is not null && a.Version != AllVersionsSpec.Instance) > 1) + .Select(g => g.Key) + .ToList(); + + if (lifecyclesWithMultipleVersions.Count > 0) { - var lifecycleVersionedItems = group.Where(a => a.Version is not null && - a.Version != AllVersionsSpec.Instance).ToList(); - if (lifecycleVersionedItems.Count > 1) - { - diagnostics.Add((Severity.Warning, - $"Key '{key}': Multiple version declarations for {group.Key} lifecycle. Only one version per lifecycle is allowed.")); - } + var lifecycleNames = string.Join(", ", lifecyclesWithMultipleVersions); + diagnostics.Add((Severity.Warning, + $"Key '{key}': Multiple version declarations found for lifecycle(s): {lifecycleNames}. Only one version per lifecycle is allowed.")); } // Rule: Only one item per key can use greater-than syntax @@ -294,37 +295,42 @@ private static void ValidateApplicabilityCollection(string key, AppliesCollectio } // Rule: In a range, the first version must be less than or equal the last version - foreach (var item in items.Where(a => a.Version is { Kind: VersionSpecKind.Range })) + var invalidRanges = items + .Where(a => a.Version is { Kind: VersionSpecKind.Range } && a.Version!.Min.CompareTo(a.Version.Max!) > 0) + .ToList(); + + if (invalidRanges.Count > 0) { - var spec = item.Version!; - if (spec.Min.CompareTo(spec.Max!) > 0) - { - diagnostics.Add((Severity.Warning, - $"Key '{key}', {item.Lifecycle}: Range has first version ({spec.Min.Major}.{spec.Min.Minor}) greater than last version ({spec.Max!.Major}.{spec.Max.Minor}).")); - } + var rangeDescriptions = invalidRanges.Select(item => + $"{item.Lifecycle} ({item.Version!.Min.Major}.{item.Version.Min.Minor}-{item.Version.Max!.Major}.{item.Version.Max.Minor})"); + diagnostics.Add((Severity.Warning, + $"Key '{key}': Invalid range(s) where first version is greater than last version: {string.Join(", ", rangeDescriptions)}.")); } // Rule: No overlapping version ranges for the same key - var versionedItems = items.Where(a => a.Version is not null && - a.Version != AllVersionsSpec.Instance).ToList(); + var versionedItems = items + .Where(a => a.Version is not null && a.Version != AllVersionsSpec.Instance) + .ToList(); - for (var i = 0; i < versionedItems.Count; i++) + var hasOverlaps = false; + for (var i = 0; i < versionedItems.Count && !hasOverlaps; i++) { - for (var j = i + 1; j < versionedItems.Count; j++) + for (var j = i + 1; j < versionedItems.Count && !hasOverlaps; j++) { - if (CheckVersionOverlap(versionedItems[i].Version!, versionedItems[j].Version!, out var overlapMsg)) - { - diagnostics.Add((Severity.Warning, - $"Key '{key}': Overlapping versions between {versionedItems[i].Lifecycle} and {versionedItems[j].Lifecycle}. {overlapMsg}")); - } + if (CheckVersionOverlap(versionedItems[i].Version!, versionedItems[j].Version!)) + hasOverlaps = true; } } + + if (hasOverlaps) + { + diagnostics.Add((Severity.Warning, + $"Key '{key}': Overlapping version ranges detected. Ensure version ranges do not overlap within the same key.")); + } } - private static bool CheckVersionOverlap(VersionSpec v1, VersionSpec v2, out string message) + private static bool CheckVersionOverlap(VersionSpec v1, VersionSpec v2) { - message = string.Empty; - // Get the effective ranges for each version spec // For GreaterThanOrEqual: [min, infinity) // For Range: [min, max] @@ -333,13 +339,8 @@ private static bool CheckVersionOverlap(VersionSpec v1, VersionSpec v2, out stri var (v1Min, v1Max) = GetEffectiveRange(v1); var (v2Min, v2Max) = GetEffectiveRange(v2); - var overlaps = v1Min.CompareTo(v2Max ?? new SemVersion(99999, 0, 0)) <= 0 && - v2Min.CompareTo(v1Max ?? new SemVersion(99999, 0, 0)) <= 0; - - if (overlaps) - message = $"Version ranges overlap."; - - return overlaps; + return v1Min.CompareTo(v2Max ?? new SemVersion(99999, 0, 0)) <= 0 && + v2Min.CompareTo(v1Max ?? new SemVersion(99999, 0, 0)) <= 0; } private static (SemVersion min, SemVersion? max) GetEffectiveRange(VersionSpec spec) => spec.Kind switch From e5741533a54a1de741da41101df292b5357afe4a Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 12:29:29 -0300 Subject: [PATCH 31/61] Adjust existing tests --- .../Applicability/ApplicableToComponent.fs | 590 ++++-------------- tests/authoring/Blocks/Admonitions.fs | 39 +- tests/authoring/Inline/AppliesToRole.fs | 42 +- 3 files changed, 141 insertions(+), 530 deletions(-) diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index 5619211fd..a4c33f96d 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -12,7 +12,7 @@ open Swensen.Unquote open Xunit // Test Stack applicability scenarios -type ``stack applicability tests`` () = +type ``stack ga future applicability`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: ga 9.0.0 @@ -31,15 +31,8 @@ stack: ga 9.0.0 let ``renders GA with version`` () = markdown |> convertsToHtml """

- - Stack - - - Planned - - + +

""" @@ -54,19 +47,12 @@ stack: preview 9.1.0 let ``renders preview future version as planned`` () = markdown |> convertsToHtml """

- - Stack - - - Planned - - + +

""" -type ``stack beta current version`` () = +type ``stack beta future version`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: beta 8.8.0 @@ -74,22 +60,15 @@ stack: beta 8.8.0 """ [] - let ``renders beta current version`` () = + let ``renders beta future version`` () = markdown |> convertsToHtml """

- - Stack - - - Planned - - + +

""" -type ``stack deprecated`` () = +type ``stack planned deprecation`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: deprecated 8.7.0 @@ -97,20 +76,15 @@ stack: deprecated 8.7.0 """ [] - let ``renders deprecated`` () = + let ``renders deprecation planned`` () = markdown |> convertsToHtml """

- - Stack - - - Deprecation planned - - + +

""" -type ``stack removed`` () = +type ``stack removal planned`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: removed 8.6.0 @@ -118,20 +92,15 @@ stack: removed 8.6.0 """ [] - let ``renders removed`` () = + let ``renders planned for removal`` () = markdown |> convertsToHtml """

- - Stack - - - Removal planned - - + +

""" -type ``stack all versions`` () = +type ``stack ga base version`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: ga @@ -139,25 +108,16 @@ stack: ga """ [] - let ``renders all versions`` () = + let ``renders ga base version`` () = markdown |> convertsToHtml """

- - Stack - - - - 8.0+ - - - + +

""" // Test Serverless applicability scenarios -type ``serverless all projects`` () = +type ``serverless ga future`` () = static let markdown = Setup.Markdown """ ```{applies_to} serverless: ga 9.0.0 @@ -165,16 +125,11 @@ serverless: ga 9.0.0 """ [] - let ``renders serverless all projects`` () = + let ``renders serverless ga planned`` () = markdown |> convertsToHtml """

- - Serverless - - - Planned - - + +

""" @@ -192,31 +147,12 @@ serverless: let ``renders serverless individual projects`` () = markdown |> convertsToHtml """

- - Serverless Elasticsearch - - - Planned - - - - Serverless Observability - - - Planned - - - - Serverless Security - - - Planned - - + + + + + +

""" @@ -233,13 +169,11 @@ deployment: let ``renders ECE deployment`` () = markdown |> convertsToHtml """

- - ECE - - - Planned - - + +

""" @@ -255,15 +189,8 @@ deployment: let ``renders ECK deployment`` () = markdown |> convertsToHtml """

- - ECK - - - Planned - - + +

""" @@ -279,16 +206,8 @@ deployment: let ``renders ECH deployment`` () = markdown |> convertsToHtml """

- - ECH - - - Planned - - -

+ +

""" type ``deployment self managed`` () = @@ -303,18 +222,13 @@ deployment: let ``renders self-managed deployment`` () = markdown |> convertsToHtml """

- - Self-Managed - - - Planned - - -

+ + +

""" // Test Product applicability scenarios -type ``apm agents`` () = +type ``apm agents future versions`` () = static let markdown = Setup.Markdown """ ```{applies_to} apm_agent_dotnet: ga 9.0.0 @@ -324,38 +238,28 @@ apm_agent_python: preview 9.2.0 """ [] - let ``renders APM agents`` () = + let ``renders APM agents planned`` () = markdown |> convertsToHtml """

- - APM Agent .NET - - - Planned - - - - APM Agent Java - - - Planned - - - - APM Agent Python - - - Planned - - + + + + + +

""" -type ``edot agents`` () = +type ``edot agents future versions`` () = static let markdown = Setup.Markdown """ ```{applies_to} edot_dotnet: ga 9.0.0 @@ -365,39 +269,20 @@ edot_python: preview 9.2.0 """ [] - let ``renders EDOT agents`` () = + let ``renders EDOT agents planned`` () = markdown |> convertsToHtml """

- - EDOT .NET - - - Planned - - - - EDOT Java - - - Planned - - - - EDOT Python - - - Planned - - + + + + + +

""" // Test complex scenarios with multiple lifecycles -type ``mixed lifecycles with ga planned`` () = +type ``mixed unreleased lifecycles falls back to preview`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: ga 8.8.0, preview 8.1.0 @@ -408,24 +293,8 @@ stack: ga 8.8.0, preview 8.1.0 let ``renders Preview when GA and Preview both exist for an unreleased entry`` () = markdown |> convertsToHtml """

- - Stack - - - Preview - - - - - - - + +

""" @@ -440,13 +309,8 @@ stack: deprecated 9.1.0 let ``renders deprecation planned for future version`` () = markdown |> convertsToHtml """

- - Stack - - - Deprecation planned - - + +

""" @@ -461,13 +325,8 @@ stack: removed 9.1.0 let ``renders removal planned for future version`` () = markdown |> convertsToHtml """

- - Stack - - - Removal planned - - + +

""" @@ -483,17 +342,9 @@ stack: unavailable let ``renders unavailable`` () = markdown |> convertsToHtml """

- - Stack - - - Unavailable - - 8.0+ - - - -

+ + +

""" type ``product all versions`` () = @@ -507,14 +358,8 @@ product: ga let ``renders product all versions`` () = markdown |> convertsToHtml """

- - - - - 8.0+ - - - + +

""" @@ -529,17 +374,8 @@ product: preview 1.3.0 let ``renders product preview`` () = markdown |> convertsToHtml """

- - - - Preview - - 1.3+ - - - + +

""" @@ -564,103 +400,24 @@ apm_agent_java: beta 9.1.0 let ``renders complex mixed scenario`` () = markdown |> convertsToHtml """

- - Serverless Elasticsearch - - - Planned - - - - Serverless Observability - - - Planned - - - - Stack - - - Planned - - - - ECK - - - Planned - - - - ECE - - - Planned - - - - APM Agent .NET - - - Planned - - - - APM Agent Java - - - Planned - - -

-""" - -// Test missing lifecycle scenarios -type ``lifecycle scenarios missing`` () = - static let markdown = Setup.Markdown """ -```{applies_to} -stack: beta 9.1.0 -deployment: - ece: ga 9.1.0 -``` -""" - - [] - let ``renders missing lifecycle scenarios`` () = - markdown |> convertsToHtml """ -

- - Stack - - - Planned - - - - ECE - - - Planned - - + + + + + + + + + + + + + +

""" -// Test missing version scenarios -type ``version scenarios missing`` () = +type ``stack and ece future versions`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: beta 9.1.0 @@ -670,30 +427,17 @@ deployment: """ [] - let ``renders missing version scenarios`` () = + let ``renders stack and ece planned`` () = markdown |> convertsToHtml """

- - Stack - - - Planned - - - - ECE - - - Planned - - + + + +

""" -// Test missing edge cases -type ``edge cases missing`` () = +type ``stack empty defaults to ga`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: @@ -701,21 +445,16 @@ stack: """ [] - let ``renders missing edge cases`` () = + let ``no version defaults to ga`` () = markdown |> convertsToHtml """

- - Stack - - - + +

""" // Test missing VersioningSystemId coverage -type ``versioning system id coverage`` () = +type ``all products future version coverage`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: ga 9.0.0 @@ -730,89 +469,29 @@ product: ga 9.0.0 """ [] - let ``renders missing VersioningSystemId coverage`` () = - markdown |> convertsToHtml """ -

- - Serverless - - - Planned - - - - Stack - - - Planned - - - - ECH - - - Planned - - - - ECK - - - Planned - - - - ECE - - - Planned - - - - Self-Managed - - - Planned - - - - - - Planned - - -

-""" - -// Test missing disclaimer scenarios -type ``disclaimer scenarios`` () = - static let markdown = Setup.Markdown """ -```{applies_to} -stack: ga 9.0.0 -``` -""" - - [] - let ``renders missing disclaimer scenarios`` () = + let ``renders VersioningSystemId coverage`` () = markdown |> convertsToHtml """

- - Stack - - - Planned - - + + + + + + + + + + + + + +

""" // Test multiple lifecycles for same applicability key // With version inference: ga 8.0, beta 8.1 → ga =8.0 (exact), beta 8.1+ (highest gets GTE) -type ``multiple lifecycles same key`` () = +type ``ga with beta uses version inference`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: ga 8.0.0, beta 8.1.0 @@ -823,26 +502,7 @@ stack: ga 8.0.0, beta 8.1.0 let ``renders multiple lifecycles with ellipsis and shows GA lifecycle`` () = markdown |> convertsToHtml """

- - Stack - - - GA - - 8.0 - - - - - - - - + +

""" diff --git a/tests/authoring/Blocks/Admonitions.fs b/tests/authoring/Blocks/Admonitions.fs index 85e738a23..bb2e5c8b4 100644 --- a/tests/authoring/Blocks/Admonitions.fs +++ b/tests/authoring/Blocks/Admonitions.fs @@ -64,18 +64,8 @@ This is a custom admonition with applies_to information.
Note - - Stack - - - - 8.0+ - - - - + +
@@ -86,16 +76,8 @@ If this functionality is unavailable or behaves differently when deployed on ECH
Warning - - Serverless - - - - 8.0+ - - - - + +
@@ -106,18 +88,7 @@ If this functionality is unavailable or behaves differently when deployed on ECH
Tip - - Serverless Elasticsearch - - - Preview - - 8.0+ - - - +
diff --git a/tests/authoring/Inline/AppliesToRole.fs b/tests/authoring/Inline/AppliesToRole.fs index e6a3f09a1..614563638 100644 --- a/tests/authoring/Inline/AppliesToRole.fs +++ b/tests/authoring/Inline/AppliesToRole.fs @@ -30,15 +30,11 @@ This is an inline {applies_to}`stack: preview 9.1` element. markdown |> convertsToHtml """

This is an inline - - Stack - - - Planned - - + + element.

""" @@ -122,7 +118,7 @@ type ``parses multiple applies_to in one line`` () = ) )) -type ``render 'GA Planned' if preview exists alongside ga`` () = +type ``render 'Preview' for GA in future version`` () = static let markdown = Setup.Markdown """ This is an inline {applies_to}`stack: preview 8.0, ga 8.1` element. @@ -141,27 +137,11 @@ This is an inline {applies_to}`stack: preview 8.0, ga 8.1` element. markdown |> convertsToHtml """

This is an inline - - Stack - - - Preview - - 8.0 - - - - - - - - + + element.

""" From f824af8e3642764b9212f86b11d2b57736f4dae0 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 12:38:43 -0300 Subject: [PATCH 32/61] Fix lint --- .../web-components/AppliesToPopover.tsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx index 23380b2d7..e56ee26ef 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx @@ -55,12 +55,12 @@ const AppliesToPopover = ({ const badgeRef = useRef(null) const hoverTimeoutRef = useRef | null>(null) - const hasPopoverContent = popoverData && ( - popoverData.productDescription || - popoverData.availabilityItems.length > 0 || - popoverData.additionalInfo || - popoverData.showVersionNote - ) + const hasPopoverContent = + popoverData && + (popoverData.productDescription || + popoverData.availabilityItems.length > 0 || + popoverData.additionalInfo || + popoverData.showVersionNote) const openPopover = useCallback(() => { if (showPopover && hasPopoverContent) { @@ -268,7 +268,13 @@ const AppliesToPopover = ({ } `} > - {item.text} + + {item.text} +
{isItemOpen && (

Date: Mon, 15 Dec 2025 12:49:58 -0300 Subject: [PATCH 33/61] Introduce more test scenarios --- .../Myst/Components/ApplicabilityRenderer.cs | 10 +- .../Applicability/ApplicableToComponent.fs | 146 ++++++++++++++++++ 2 files changed, 150 insertions(+), 6 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index e775e6fb1..853d945f3 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -346,7 +346,8 @@ private static string GetPlainProductName(string displayName) => displayName.Replace(" ", " "); ///

- /// Determines if a version should be considered released. + /// Determines if a version should be considered released for lifecycle description purposes + /// For ranges, if min is released, the feature is currently available /// private static bool IsVersionReleased(Applicability applicability, VersioningSystem versioningSystem) { @@ -356,11 +357,8 @@ private static bool IsVersionReleased(Applicability applicability, VersioningSys if (versionSpec is null or AllVersionsSpec) return true; - // For ranges, check the max version - if (versionSpec.Kind == VersionSpecKind.Range && versionSpec.Max is not null) - return versionSpec.Max <= versioningSystem.Current; - - // For GTE and Exact, check the min version + // For all version spec types, check if min is released + // This determines whether the feature is currently available return versionSpec.Min <= versioningSystem.Current; } diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index a4c33f96d..a74680d4e 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -506,3 +506,149 @@ stack: ga 8.0.0, beta 8.1.0

""" + +type ``stack ga released version`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: ga 7.0.0 +``` +""" + + [] + let ``renders ga since released version`` () = + markdown |> convertsToHtml """ +

+ + +

+""" + +type ``stack preview released version`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: preview 7.0.0 +``` +""" + + [] + let ``renders preview since released version`` () = + markdown |> convertsToHtml """ +

+ + +

+""" + +type ``stack beta released version`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: beta 7.0.0 +``` +""" + + [] + let ``renders beta since released version`` () = + markdown |> convertsToHtml """ +

+ + +

+""" + +type ``stack deprecated released version`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: deprecated 7.0.0 +``` +""" + + [] + let ``renders deprecated since released version`` () = + markdown |> convertsToHtml """ +

+ + +

+""" + +type ``stack removed released version`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: removed 7.0.0 +``` +""" + + [] + let ``renders removed in released version`` () = + markdown |> convertsToHtml """ +

+ + +

+""" + +// Version spec syntax tests (exact and range) +type ``stack ga exact version released`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: ga =7.5 +``` +""" + + [] + let ``renders ga in exact released version`` () = + markdown |> convertsToHtml """ +

+ + +

+""" + +type ``stack ga range both released`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: ga 7.0-8.0 +``` +""" + + [] + let ``renders ga from-to when both ends released`` () = + markdown |> convertsToHtml """ +

+ + +

+""" + +type ``stack ga range max unreleased`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: ga 7.0-9.0 +``` +""" + + [] + let ``renders ga since min when max unreleased`` () = + markdown |> convertsToHtml """ +

+ + +

+""" + +// Multiple released lifecycles showing both in popover +type ``preview and ga both released`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: preview 7.0, ga 7.5 +``` +""" + + [] + let ``renders ga badge with both lifecycles in popover`` () = + markdown |> convertsToHtml """ +

+ + +

+""" From 9e2875e50e6b8e051b0ce6dc226abb6653901835 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 13:00:31 -0300 Subject: [PATCH 34/61] Adjust cursor behavior on desktop --- .../Assets/markdown/applies-to.css | 35 +++++++++++++++---- .../web-components/AppliesToPopover.tsx | 33 ++++++++++++----- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css index dc59efa8e..1e74fec30 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css @@ -13,7 +13,8 @@ } .applicable-info--clickable { - @apply cursor-pointer; + /* Desktop: tooltip-like behavior, no pointer cursor */ + @apply cursor-default; &:hover { @apply border-grey-30 bg-grey-10; @@ -23,14 +24,36 @@ outline: none; } - &:focus-visible { - outline: 2px solid var(--color-blue-elastic); - outline-offset: 2px; + /* Desktop: no focus-visible outline for tooltip behavior */ + @media (hover: hover) and (pointer: fine) { + &:focus-visible { + outline: none; + } + } + + /* Mobile/touch: show focus outline for accessibility */ + @media (hover: none), (pointer: coarse) { + @apply cursor-pointer; + + &:focus-visible { + outline: 2px solid var(--color-blue-elastic); + outline-offset: 2px; + } } } - .applicable-info--pinned { - @apply border-blue-elastic bg-grey-10; + /* Desktop: no pinned state styling */ + @media (hover: hover) and (pointer: fine) { + .applicable-info--pinned { + @apply border-grey-20 bg-white; + } + } + + /* Mobile/touch: show pinned state */ + @media (hover: none), (pointer: coarse) { + .applicable-info--pinned { + @apply border-blue-elastic bg-grey-10; + } } .applicable-meta { diff --git a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx index e56ee26ef..2c965b77f 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx @@ -50,11 +50,25 @@ const AppliesToPopover = ({ const [isOpen, setIsOpen] = useState(false) const [isPinned, setIsPinned] = useState(false) const [openItems, setOpenItems] = useState>(new Set()) + const [isTouchDevice, setIsTouchDevice] = useState(false) const popoverId = useGeneratedHtmlId({ prefix: 'appliesToPopover' }) const contentRef = useRef(null) const badgeRef = useRef(null) const hoverTimeoutRef = useRef | null>(null) + // Detect touch device on mount + useEffect(() => { + const checkTouchDevice = () => { + const hasCoarsePointer = window.matchMedia('(pointer: coarse)').matches + const hasNoHover = window.matchMedia('(hover: none)').matches + setIsTouchDevice(hasCoarsePointer || hasNoHover) + } + checkTouchDevice() + // Re-check on resize in case device mode changes (e.g., dev tools toggle) + window.addEventListener('resize', checkTouchDevice) + return () => window.removeEventListener('resize', checkTouchDevice) + }, []) + const hasPopoverContent = popoverData && (popoverData.productDescription || @@ -75,6 +89,10 @@ const AppliesToPopover = ({ }, [isPinned]) const handleClick = useCallback(() => { + // Only allow click/pin behavior on touch devices + // On desktop, the popover is tooltip-like (hover only) + if (!isTouchDevice) return + if (showPopover && hasPopoverContent) { if (isPinned) { // If already pinned, unpin and close @@ -86,7 +104,7 @@ const AppliesToPopover = ({ setIsOpen(true) } } - }, [showPopover, hasPopoverContent, isPinned]) + }, [showPopover, hasPopoverContent, isPinned, isTouchDevice]) const toggleItem = useCallback((index: number, e: React.MouseEvent) => { e.stopPropagation() @@ -165,6 +183,9 @@ const AppliesToPopover = ({ const showSeparator = badgeKey && (showLifecycleName || showVersion || badgeLifecycleText) + // Only show interactive attributes on touch devices + const isInteractive = showPopover && hasPopoverContent && isTouchDevice + const badgeButton = ( { - if ( - showPopover && - hasPopoverContent && - (e.key === 'Enter' || e.key === ' ') - ) { + if (isInteractive && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault() handleClick() } From 38d912b89e5b875bfe5c7fa524c6e7de4e202f4b Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 13:15:47 -0300 Subject: [PATCH 35/61] Only use 'since' when a version is set --- .../Myst/Components/ApplicabilityRenderer.cs | 5 +++-- tests/authoring/Applicability/ApplicableToComponent.fs | 8 ++++---- tests/authoring/Blocks/Admonitions.fs | 6 +++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 853d945f3..c5650f5bb 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -220,8 +220,9 @@ private static PopoverData BuildPopoverData( var baseVersion = $"{versioningSystem.Base.Major}.{versioningSystem.Base.Minor}"; return lifecycle switch { - ProductLifecycle.Removed => $"Removed in {baseVersion}", - _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} since {baseVersion}" + ProductLifecycle.Removed => $"Removed in {baseVersion}+", + ProductLifecycle.Unavailable => $"Unavailable in {baseVersion}+", + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} in {baseVersion}+" }; } diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index a74680d4e..9eef03326 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -111,7 +111,7 @@ stack: ga let ``renders ga base version`` () = markdown |> convertsToHtml """

- +

""" @@ -342,7 +342,7 @@ stack: unavailable let ``renders unavailable`` () = markdown |> convertsToHtml """

- +

""" @@ -358,7 +358,7 @@ product: ga let ``renders product all versions`` () = markdown |> convertsToHtml """

- +

""" @@ -448,7 +448,7 @@ stack: let ``no version defaults to ga`` () = markdown |> convertsToHtml """

- +

""" diff --git a/tests/authoring/Blocks/Admonitions.fs b/tests/authoring/Blocks/Admonitions.fs index bb2e5c8b4..63525a2c2 100644 --- a/tests/authoring/Blocks/Admonitions.fs +++ b/tests/authoring/Blocks/Admonitions.fs @@ -64,7 +64,7 @@ This is a custom admonition with applies_to information.
Note - +
@@ -76,7 +76,7 @@ This is a custom admonition with applies_to information.
Warning - +
@@ -88,7 +88,7 @@ This is a custom admonition with applies_to information.
Tip - +
From ea8776fff4f02dd3ad04e98744d999ef60c4e53e Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 17:47:28 -0300 Subject: [PATCH 36/61] Fix presentation of applicability details --- .../web-components/AppliesToPopover.tsx | 3 +- .../Myst/Components/ApplicabilityRenderer.cs | 44 +++++------ .../Applicability/ApplicableToComponent.fs | 78 +++++++++---------- tests/authoring/Inline/AppliesToRole.fs | 4 +- 4 files changed, 64 insertions(+), 65 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx index 2c965b77f..b39b9a1e6 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx @@ -59,7 +59,8 @@ const AppliesToPopover = ({ // Detect touch device on mount useEffect(() => { const checkTouchDevice = () => { - const hasCoarsePointer = window.matchMedia('(pointer: coarse)').matches + const hasCoarsePointer = + window.matchMedia('(pointer: coarse)').matches const hasNoHover = window.matchMedia('(hover: none)').matches setIsTouchDevice(hasCoarsePointer || hasNoHover) } diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index c5650f5bb..32124ee5a 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -239,11 +239,11 @@ private static PopoverData BuildPopoverData( // Greater than or equal (x.x+, x.x, x.x.x+, x.x.x) VersionSpecKind.GreaterThanOrEqual => GenerateGteAvailabilityText(lifecycle, minVersion, isMinReleased, lifecycleCount), - // ange (x.x-y.y, x.x.x-y.y.y) - VersionSpecKind.Range => GenerateRangeAvailabilityText(lifecycle, minVersion, maxVersion!, isMinReleased, isMaxReleased, lifecycleCount), + // Range (x.x-y.y, x.x.x-y.y.y) + VersionSpecKind.Range => GenerateRangeAvailabilityText(lifecycle, minVersion, maxVersion!, isMinReleased, isMaxReleased), // Exact (=x.x, =x.x.x) - VersionSpecKind.Exact => GenerateExactAvailabilityText(lifecycle, minVersion, isMinReleased, lifecycleCount), + VersionSpecKind.Exact => GenerateExactAvailabilityText(lifecycle, minVersion, isMinReleased), _ => null }; @@ -264,15 +264,15 @@ private static PopoverData BuildPopoverData( }; } - // Unreleased + // Unreleased - show the version so users see the full timeline in the popover + // Only hide Unavailable when there are other lifecycles (it's implied by other entries) return lifecycle switch { - ProductLifecycle.Deprecated => "Planned for deprecation", - ProductLifecycle.Removed => "Planned for removal", + ProductLifecycle.Deprecated => $"Deprecated since {version}", + ProductLifecycle.Removed => $"Removed in {version}", ProductLifecycle.Unavailable when lifecycleCount == 1 => "Unavailable", - ProductLifecycle.Unavailable => null, // Do not add to availability list - _ when lifecycleCount == 1 => "Planned", - _ => null // Do not add to availability list + ProductLifecycle.Unavailable => null, + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} since {version}" }; } @@ -280,7 +280,7 @@ private static PopoverData BuildPopoverData( /// Generates availability text for range version type. /// private static string? GenerateRangeAvailabilityText( - ProductLifecycle lifecycle, string minVersion, string maxVersion, bool isMinReleased, bool isMaxReleased, int lifecycleCount) + ProductLifecycle lifecycle, string minVersion, string maxVersion, bool isMinReleased, bool isMaxReleased) { if (isMaxReleased) { @@ -303,21 +303,20 @@ private static PopoverData BuildPopoverData( }; } - // Neither released + // Neither released - show the version so users see the full timeline in the popover return lifecycle switch { - ProductLifecycle.Deprecated => "Planned for deprecation", - ProductLifecycle.Removed => "Planned for removal", - ProductLifecycle.Unavailable => null, // Do not add to availability list - _ when lifecycleCount == 1 => "Planned", - _ => null // Do not add to availability list + ProductLifecycle.Deprecated => $"Deprecated since {minVersion}", + ProductLifecycle.Removed => $"Removed in {minVersion}", + ProductLifecycle.Unavailable => null, + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} since {minVersion}" }; } /// /// Generates availability text for exact version type. /// - private static string? GenerateExactAvailabilityText(ProductLifecycle lifecycle, string version, bool isReleased, int lifecycleCount) + private static string? GenerateExactAvailabilityText(ProductLifecycle lifecycle, string version, bool isReleased) { if (isReleased) { @@ -329,14 +328,13 @@ private static PopoverData BuildPopoverData( }; } - // Unreleased + // Unreleased - show the version so users see the full timeline in the popover return lifecycle switch { - ProductLifecycle.Deprecated => "Planned for deprecation", - ProductLifecycle.Removed => "Planned for removal", - ProductLifecycle.Unavailable => null, // Do not add to availability list - _ when lifecycleCount == 1 => "Planned", - _ => null // Do not add to availability list + ProductLifecycle.Deprecated => $"Deprecated in {version}", + ProductLifecycle.Removed => $"Removed in {version}", + ProductLifecycle.Unavailable => null, + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} in {version}" }; } diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index 9eef03326..3c20915e0 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -31,7 +31,7 @@ stack: ga 9.0.0 let ``renders GA with version`` () = markdown |> convertsToHtml """

- +

""" @@ -47,7 +47,7 @@ stack: preview 9.1.0 let ``renders preview future version as planned`` () = markdown |> convertsToHtml """

- +

""" @@ -63,7 +63,7 @@ stack: beta 8.8.0 let ``renders beta future version`` () = markdown |> convertsToHtml """

- +

""" @@ -79,7 +79,7 @@ stack: deprecated 8.7.0 let ``renders deprecation planned`` () = markdown |> convertsToHtml """

- +

""" @@ -95,7 +95,7 @@ stack: removed 8.6.0 let ``renders planned for removal`` () = markdown |> convertsToHtml """

- +

""" @@ -128,7 +128,7 @@ serverless: ga 9.0.0 let ``renders serverless ga planned`` () = markdown |> convertsToHtml """

- +

""" @@ -147,11 +147,11 @@ serverless: let ``renders serverless individual projects`` () = markdown |> convertsToHtml """

- + - + - +

""" @@ -171,7 +171,7 @@ deployment:

@@ -189,7 +189,7 @@ deployment: let ``renders ECK deployment`` () = markdown |> convertsToHtml """

- +

""" @@ -206,7 +206,7 @@ deployment: let ``renders ECH deployment`` () = markdown |> convertsToHtml """

- +

""" @@ -222,7 +222,7 @@ deployment: let ``renders self-managed deployment`` () = markdown |> convertsToHtml """

- +

""" @@ -243,17 +243,17 @@ apm_agent_python: preview 9.2.0

@@ -272,11 +272,11 @@ edot_python: preview 9.2.0 let ``renders EDOT agents planned`` () = markdown |> convertsToHtml """

- + - + - +

""" @@ -293,7 +293,7 @@ stack: ga 8.8.0, preview 8.1.0 let ``renders Preview when GA and Preview both exist for an unreleased entry`` () = markdown |> convertsToHtml """

- +

""" @@ -309,7 +309,7 @@ stack: deprecated 9.1.0 let ``renders deprecation planned for future version`` () = markdown |> convertsToHtml """

- +

""" @@ -325,7 +325,7 @@ stack: removed 9.1.0 let ``renders removal planned for future version`` () = markdown |> convertsToHtml """

- +

""" @@ -400,19 +400,19 @@ apm_agent_java: beta 9.1.0 let ``renders complex mixed scenario`` () = markdown |> convertsToHtml """

- + - + - + - + - + - + - +

""" @@ -430,9 +430,9 @@ deployment: let ``renders stack and ece planned`` () = markdown |> convertsToHtml """

- + - +

""" @@ -472,19 +472,19 @@ product: ga 9.0.0 let ``renders VersioningSystemId coverage`` () = markdown |> convertsToHtml """

- + - + - + - + - + - + - +

""" @@ -502,7 +502,7 @@ stack: ga 8.0.0, beta 8.1.0 let ``renders multiple lifecycles with ellipsis and shows GA lifecycle`` () = markdown |> convertsToHtml """

- +

""" diff --git a/tests/authoring/Inline/AppliesToRole.fs b/tests/authoring/Inline/AppliesToRole.fs index 614563638..d3d5c6852 100644 --- a/tests/authoring/Inline/AppliesToRole.fs +++ b/tests/authoring/Inline/AppliesToRole.fs @@ -32,7 +32,7 @@ This is an inline {applies_to}`stack: preview 9.1` element. @@ -139,7 +139,7 @@ This is an inline {applies_to}`stack: preview 8.0, ga 8.1` element. From 6852739ea279a0ae84b859b7bb72a5ac97d5e304 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 18:02:13 -0300 Subject: [PATCH 37/61] Readjust applicability ordering --- .../Myst/Components/ApplicabilityRenderer.cs | 9 +++------ tests/authoring/Applicability/ApplicableToComponent.fs | 2 +- tests/authoring/Inline/AppliesToRole.fs | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 32124ee5a..24832ce2d 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -9,7 +9,7 @@ namespace Elastic.Markdown.Myst.Components; -public class ApplicabilityRenderer +public static class ApplicabilityRenderer { /// /// Represents a single availability item in the popover (e.g., "Generally available since 9.1"). @@ -139,12 +139,9 @@ private static PopoverData BuildPopoverData( var productName = GetPlainProductName(applicabilityDefinition.DisplayName); // Availability section - collect items from all applicabilities - // Order by: available first (by version desc), then future (by version asc) + // Order by version descending (most recent/future first, then going backwards) var orderedApplicabilities = applicabilities - .OrderByDescending(a => a.Version is null || a.Version is AllVersionsSpec || - (a.Version is { } vs && vs.Min <= versioningSystem.Current) ? 1 : 0) - .ThenByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) - .ThenBy(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) + .OrderByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) .ToList(); var availabilityItems = new List(); diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index 3c20915e0..c7bc22102 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -502,7 +502,7 @@ stack: ga 8.0.0, beta 8.1.0 let ``renders multiple lifecycles with ellipsis and shows GA lifecycle`` () = markdown |> convertsToHtml """

- +

""" diff --git a/tests/authoring/Inline/AppliesToRole.fs b/tests/authoring/Inline/AppliesToRole.fs index d3d5c6852..12cd244d3 100644 --- a/tests/authoring/Inline/AppliesToRole.fs +++ b/tests/authoring/Inline/AppliesToRole.fs @@ -139,7 +139,7 @@ This is an inline {applies_to}`stack: preview 8.0, ga 8.1` element. From 1899559fd377e9921d18d4a6b1556a314daf255a Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 18:22:48 -0300 Subject: [PATCH 38/61] Adjust version display when no lifecycles are stated --- docs/testing/req.md | 3 +++ src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs | 1 - tests/authoring/Applicability/ApplicableToComponent.fs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index 2c1c8f7a2..1e4c3d9dd 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -101,6 +101,7 @@ deployment: eck: beta 3.0, ga 3.1 ``` + ## Additional content To follow this tutorial you will need to install the following components: @@ -115,3 +116,5 @@ The tutorial assumes that you have no previous knowledge of Elasticsearch or gen - The command prompt or terminal application in your operating system. {applies_to}`ece: removed` + +{applies_to}`ece: ` diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 24832ce2d..12cefb3db 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -412,7 +412,6 @@ private static string GetBadgeVersionText(VersionSpec? versionSpec, VersioningSy switch (versionSpec) { case AllVersionsSpec: - return string.Empty; case null: // Only show base version if the product is versioned return versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index c7bc22102..915fadadc 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -448,7 +448,7 @@ stack: let ``no version defaults to ga`` () = markdown |> convertsToHtml """

- +

""" From 501fb5e8a975daab9052cfe5bb1f9155e8305b06 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 19:07:17 -0300 Subject: [PATCH 39/61] Handle multiple future versions --- docs/testing/req.md | 6 ++ .../AppliesTo/ApplicabilitySelector.cs | 20 +++--- .../Myst/Components/ApplicabilityRenderer.cs | 72 +++++++++++-------- .../Myst/Components/ApplicableToViewModel.cs | 10 +-- 4 files changed, 61 insertions(+), 47 deletions(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index 1e4c3d9dd..08d994403 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -101,6 +101,12 @@ deployment: eck: beta 3.0, ga 3.1 ``` +### Handling multiple future versions + +```{applies_to} +eck: beta 3.4, ga 3.5, deprecated 3.9 +``` + ## Additional content diff --git a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs index 4dcc98494..483021d98 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs @@ -15,9 +15,9 @@ public static class ApplicabilitySelector /// The collection of applicabilities to select from /// The current version to use for comparison /// The most relevant applicability for display - public static Applicability GetPrimaryApplicability(IEnumerable applicabilities, SemVersion currentVersion) + public static Applicability GetPrimaryApplicability(IReadOnlyCollection applicabilities, SemVersion currentVersion) { - var applicabilityList = applicabilities.ToList(); + //var applicabilityList = applicabilities.ToList(); var lifecycleOrder = new Dictionary { [ProductLifecycle.GenerallyAvailable] = 0, @@ -29,11 +29,10 @@ public static Applicability GetPrimaryApplicability(IEnumerable a [ProductLifecycle.Unavailable] = 6 }; - var availableApplicabilities = applicabilityList - .Where(a => a.Version is null || a.Version is AllVersionsSpec || a.Version.Min <= currentVersion) - .ToList(); + var availableApplicabilities = applicabilities + .Where(a => a.Version is null || a.Version is AllVersionsSpec || a.Version.Min <= currentVersion).ToArray(); - if (availableApplicabilities.Count != 0) + if (availableApplicabilities.Length > 0) { return availableApplicabilities .OrderByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) @@ -41,11 +40,10 @@ public static Applicability GetPrimaryApplicability(IEnumerable a .First(); } - var futureApplicabilities = applicabilityList - .Where(a => a.Version is not null && a.Version is not AllVersionsSpec && a.Version.Min > currentVersion) - .ToList(); + var futureApplicabilities = applicabilities + .Where(a => a.Version is not null && a.Version is not AllVersionsSpec && a.Version.Min > currentVersion).ToArray(); - if (futureApplicabilities.Count != 0) + if (futureApplicabilities.Length > 0) { return futureApplicabilities .OrderBy(a => a.Version!.Min.CompareTo(currentVersion)) @@ -53,6 +51,6 @@ public static Applicability GetPrimaryApplicability(IEnumerable a .First(); } - return applicabilityList.First(); + return applicabilities.First(); } } diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 12cefb3db..3211ebc11 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -42,38 +42,57 @@ public record ApplicabilityRenderData( ); public static ApplicabilityRenderData RenderApplicability( - IEnumerable applicabilities, + IReadOnlyCollection applicabilities, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, VersioningSystem versioningSystem) { - var applicabilityList = applicabilities.ToList(); - var allApplications = new AppliesCollection([.. applicabilityList]); + var allApplications = new AppliesCollection([.. applicabilities]); // Sort by lifecycle priority (GA > Beta > Preview > etc.) to determine display order - var sortedApplicabilities = applicabilityList + var sortedApplicabilities = applicabilities .OrderBy(a => ProductLifecycleInfo.GetOrder(a.Lifecycle)) .ThenByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) .ToList(); - var primaryLifecycle = sortedApplicabilities.First(); - var primaryBadgeData = GetBadgeData(primaryLifecycle, versioningSystem, allApplications); + // Find the first lifecycle that returns displayable badge data (non-empty text or version) + // If all return empty (all unreleased with multiple lifecycles), use the first one and show "Planned" + BadgeData? badgeData = null; + Applicability? applicabilityToDisplay = null; + BadgeData? firstBadgeData = null; + Applicability? firstApplicability = null; - // If the primary lifecycle returns an empty badge text (indicating "use previous lifecycle") - // and we have multiple lifecycles, use the next lifecycle in priority order - var applicabilityToDisplay = string.IsNullOrEmpty(primaryBadgeData.BadgeLifecycleText) && - string.IsNullOrEmpty(primaryBadgeData.Version) && - sortedApplicabilities.Count >= 2 - ? sortedApplicabilities[1] - : primaryLifecycle; + foreach (var applicability in sortedApplicabilities) + { + var candidateBadgeData = GetBadgeData(applicability, versioningSystem, allApplications); - var badgeData = applicabilityToDisplay == primaryLifecycle - ? primaryBadgeData - : GetBadgeData(applicabilityToDisplay, versioningSystem, allApplications); + // Keep track of the first one as fallback + firstBadgeData ??= candidateBadgeData; + firstApplicability ??= applicability; - var popoverData = BuildPopoverData(applicabilityList, applicabilityDefinition, versioningSystem); + // If this candidate has displayable data, use it + if (!string.IsNullOrEmpty(candidateBadgeData.BadgeLifecycleText) || + !string.IsNullOrEmpty(candidateBadgeData.Version)) + { + badgeData = candidateBadgeData; + applicabilityToDisplay = applicability; + break; + } + } + + // If we've exhausted all options (none had displayable data), use the first one with "Planned" + if (badgeData is null && firstBadgeData is not null) + { + badgeData = firstBadgeData with { BadgeLifecycleText = "Planned" }; + applicabilityToDisplay = firstApplicability; + } + + badgeData ??= GetBadgeData(sortedApplicabilities.First(), versioningSystem, allApplications); + applicabilityToDisplay ??= sortedApplicabilities.First(); + + var popoverData = BuildPopoverData(applicabilities, applicabilityDefinition, versioningSystem); // Check if there are multiple different lifecycles - var hasMultipleLifecycles = applicabilityList.Select(a => a.Lifecycle).Distinct().Count() > 1; + var hasMultipleLifecycles = applicabilities.Select(a => a.Lifecycle).Distinct().Count() > 1; return new ApplicabilityRenderData( BadgeLifecycleText: badgeData.BadgeLifecycleText, @@ -131,7 +150,7 @@ bool ShowVersion ); private static PopoverData BuildPopoverData( - List applicabilities, + IReadOnlyCollection applicabilities, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, VersioningSystem versioningSystem) { @@ -141,23 +160,14 @@ private static PopoverData BuildPopoverData( // Availability section - collect items from all applicabilities // Order by version descending (most recent/future first, then going backwards) var orderedApplicabilities = applicabilities - .OrderByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) - .ToList(); - - var availabilityItems = new List(); - foreach (var applicability in orderedApplicabilities) - { - var item = BuildAvailabilityItem(applicability, versioningSystem, productName, applicabilities.Count); - if (item is not null) - availabilityItems.Add(item); - } + .OrderByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)); var showVersionNote = productInfo is { IncludeVersionNote: true } && versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major; return new PopoverData( ProductDescription: productInfo?.Description, - AvailabilityItems: availabilityItems.ToArray(), + AvailabilityItems: orderedApplicabilities.Select(applicability => BuildAvailabilityItem(applicability, versioningSystem, productName, applicabilities.Count)).OfType().ToArray(), AdditionalInfo: productInfo?.AdditionalAvailabilityInfo, ShowVersionNote: showVersionNote, VersionNote: showVersionNote ? ProductDescriptions.VersionNote : null @@ -165,7 +175,7 @@ private static PopoverData BuildPopoverData( } /// - /// Builds an availability item for an applicability. + /// Builds an availability item for an applicability entry. /// Returns null if the item should not be added to the availability list. /// private static PopoverAvailabilityItem? BuildAvailabilityItem( diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs index cfbe91fce..ffe651543 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs @@ -58,7 +58,7 @@ public class ApplicableToViewModel }; - public IEnumerable GetApplicabilityItems() + public IReadOnlyCollection GetApplicabilityItems() { var rawItems = new List(); @@ -81,7 +81,7 @@ public IEnumerable GetApplicabilityItems() if (AppliesTo.Product is not null) rawItems.AddRange(CollectFromCollection(AppliesTo.Product, ApplicabilityMappings.Product)); - return RenderGroupedItems(rawItems); + return RenderGroupedItems(rawItems).ToArray(); } /// @@ -99,7 +99,7 @@ private static IEnumerable CollectFromCollection( /// /// Collects raw applicability items from mapped collections. /// - private static IEnumerable CollectFromMappings( + private static IReadOnlyCollection CollectFromMappings( T source, Dictionary, ApplicabilityMappings.ApplicabilityDefinition> mappings) { @@ -118,7 +118,7 @@ private static IEnumerable CollectFromMappings( /// /// Groups raw items by key and renders each group using the unified renderer. /// - private IEnumerable RenderGroupedItems(List rawItems) => + private IEnumerable RenderGroupedItems(IReadOnlyCollection rawItems) => rawItems .GroupBy(item => item.Key) .Select(group => @@ -126,7 +126,7 @@ private IEnumerable RenderGroupedItems(List i.Applicability).ToList(); + var allApplicabilities = items.Select(i => i.Applicability).ToArray(); var renderData = ApplicabilityRenderer.RenderApplicability( allApplicabilities, From 2359938214da74f3d849128f239336d076b25939 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 19:48:26 -0300 Subject: [PATCH 40/61] Fix test and ranges between versions --- docs/testing/req.md | 2 ++ src/Elastic.Documentation/AppliesTo/Applicability.cs | 2 +- tests/authoring/Applicability/ApplicableToComponent.fs | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index 08d994403..28b5270e2 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -124,3 +124,5 @@ The tutorial assumes that you have no previous knowledge of Elasticsearch or gen {applies_to}`ece: removed` {applies_to}`ece: ` + +{applies_to}:`stack: deprecated 7.16.0, removed 8.0.0` \ No newline at end of file diff --git a/src/Elastic.Documentation/AppliesTo/Applicability.cs b/src/Elastic.Documentation/AppliesTo/Applicability.cs index 7eee7debb..a2d1149a3 100644 --- a/src/Elastic.Documentation/AppliesTo/Applicability.cs +++ b/src/Elastic.Documentation/AppliesTo/Applicability.cs @@ -100,7 +100,7 @@ private static List InferVersionSemantics(List app versionMapping[currentVersion] = VersionSpec.Exact(currentVersion); else { - var rangeEnd = new SemVersion(nextVersion.Major, nextVersion.Minor - 1, 0); + var rangeEnd = new SemVersion(nextVersion.Major, nextVersion.Minor == 0 ? nextVersion.Minor : nextVersion.Minor - 1, 0); versionMapping[currentVersion] = VersionSpec.Range(currentVersion, rangeEnd); } } diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index 915fadadc..244d917b5 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -282,7 +282,7 @@ edot_python: preview 9.2.0 """ // Test complex scenarios with multiple lifecycles -type ``mixed unreleased lifecycles falls back to preview`` () = +type ``mixed unreleased lifecycles falls back to planned`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: ga 8.8.0, preview 8.1.0 @@ -290,10 +290,10 @@ stack: ga 8.8.0, preview 8.1.0 """ [] - let ``renders Preview when GA and Preview both exist for an unreleased entry`` () = + let ``renders Planned when GA and Preview are both unreleased`` () = markdown |> convertsToHtml """

- +

""" From 3af43c4338e8b21b65f2c75bfa4034883c3d81d9 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 20:05:45 -0300 Subject: [PATCH 41/61] Typo --- docs/testing/req.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index 28b5270e2..aaaf5f405 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -125,4 +125,4 @@ The tutorial assumes that you have no previous knowledge of Elasticsearch or gen {applies_to}`ece: ` -{applies_to}:`stack: deprecated 7.16.0, removed 8.0.0` \ No newline at end of file +{applies_to}`stack: deprecated 7.16.0, removed 8.0.0` From 4f7f107ac6a3fbf9750aaeaef367536088629cd9 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 20:10:36 -0300 Subject: [PATCH 42/61] Remove unused value --- .../Myst/Components/ApplicabilityRenderer.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 3211ebc11..1b3eb99e5 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -57,7 +57,6 @@ public static ApplicabilityRenderData RenderApplicability( // Find the first lifecycle that returns displayable badge data (non-empty text or version) // If all return empty (all unreleased with multiple lifecycles), use the first one and show "Planned" BadgeData? badgeData = null; - Applicability? applicabilityToDisplay = null; BadgeData? firstBadgeData = null; Applicability? firstApplicability = null; @@ -74,20 +73,15 @@ public static ApplicabilityRenderData RenderApplicability( !string.IsNullOrEmpty(candidateBadgeData.Version)) { badgeData = candidateBadgeData; - applicabilityToDisplay = applicability; break; } } // If we've exhausted all options (none had displayable data), use the first one with "Planned" if (badgeData is null && firstBadgeData is not null) - { badgeData = firstBadgeData with { BadgeLifecycleText = "Planned" }; - applicabilityToDisplay = firstApplicability; - } badgeData ??= GetBadgeData(sortedApplicabilities.First(), versioningSystem, allApplications); - applicabilityToDisplay ??= sortedApplicabilities.First(); var popoverData = BuildPopoverData(applicabilities, applicabilityDefinition, versioningSystem); From 8bf465838345e2b1393f12ade5bfeeb28e21505a Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 20:35:49 -0300 Subject: [PATCH 43/61] Allow special rule for ranges --- docs/testing/req.md | 2 ++ .../AppliesTo/ApplicableToYamlConverter.cs | 10 +++++++++- .../Myst/Components/ApplicabilityRenderer.cs | 4 +++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index aaaf5f405..76d8560a3 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -126,3 +126,5 @@ The tutorial assumes that you have no previous knowledge of Elasticsearch or gen {applies_to}`ece: ` {applies_to}`stack: deprecated 7.16.0, removed 8.0.0` + +{applies_to}`ess: ` \ No newline at end of file diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs index 211bbfdc4..fe1ec46a0 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs @@ -307,7 +307,7 @@ private static void ValidateApplicabilityCollection(string key, AppliesCollectio $"Key '{key}': Invalid range(s) where first version is greater than last version: {string.Join(", ", rangeDescriptions)}.")); } - // Rule: No overlapping version ranges for the same key + // Rule: No overlapping version ranges var versionedItems = items .Where(a => a.Version is not null && a.Version != AllVersionsSpec.Instance) .ToList(); @@ -331,6 +331,14 @@ private static void ValidateApplicabilityCollection(string key, AppliesCollectio private static bool CheckVersionOverlap(VersionSpec v1, VersionSpec v2) { + // Special case: two gte specs with different min versions represent a version progression + // e.g., "deprecated 7.16.0, removed 8.0.0" - these don't truly overlap, version inference + // will convert the earlier one to a range + if (v1.Kind == VersionSpecKind.GreaterThanOrEqual && + v2.Kind == VersionSpecKind.GreaterThanOrEqual && + v1.Min.CompareTo(v2.Min) != 0) + return false; + // Get the effective ranges for each version spec // For GreaterThanOrEqual: [min, infinity) // For Range: [min, max] diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 1b3eb99e5..c6a461959 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -78,7 +78,9 @@ public static ApplicabilityRenderData RenderApplicability( } // If we've exhausted all options (none had displayable data), use the first one with "Planned" - if (badgeData is null && firstBadgeData is not null) + // But only for versioned products - unversioned products (base=99999) should show empty badge + var isVersionedProduct = versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major; + if (badgeData is null && firstBadgeData is not null && isVersionedProduct) badgeData = firstBadgeData with { BadgeLifecycleText = "Planned" }; badgeData ??= GetBadgeData(sortedApplicabilities.First(), versioningSystem, allApplications); From ebca4ffd4b905b35a03ae21458de66fb676404da Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Mon, 15 Dec 2025 22:04:22 -0300 Subject: [PATCH 44/61] Allow version bump overlaps --- .../AppliesTo/ApplicableToYamlConverter.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs index fe1ec46a0..208291f9d 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs @@ -331,12 +331,12 @@ private static void ValidateApplicabilityCollection(string key, AppliesCollectio private static bool CheckVersionOverlap(VersionSpec v1, VersionSpec v2) { - // Special case: two gte specs with different min versions represent a version progression - // e.g., "deprecated 7.16.0, removed 8.0.0" - these don't truly overlap, version inference - // will convert the earlier one to a range - if (v1.Kind == VersionSpecKind.GreaterThanOrEqual && - v2.Kind == VersionSpecKind.GreaterThanOrEqual && - v1.Min.CompareTo(v2.Min) != 0) + // Allow overlap in case there is a version bump + if (v1.Kind == VersionSpecKind.Range && v2.Kind == VersionSpecKind.GreaterThanOrEqual && + v1.Max is not null && v1.Max.CompareTo(v2.Min) <= 0) + return false; + if (v2.Kind == VersionSpecKind.Range && v1.Kind == VersionSpecKind.GreaterThanOrEqual && + v2.Max is not null && v2.Max.CompareTo(v1.Min) <= 0) return false; // Get the effective ranges for each version spec From 0be4340fb7cbc1016e9e522d8e4c4503aa552a2c Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Tue, 16 Dec 2025 06:58:08 -0300 Subject: [PATCH 45/61] Adjust availability list item display --- .../Myst/Components/ApplicabilityRenderer.cs | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index c6a461959..7ffd0471f 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -243,10 +243,10 @@ private static PopoverData BuildPopoverData( VersionSpecKind.GreaterThanOrEqual => GenerateGteAvailabilityText(lifecycle, minVersion, isMinReleased, lifecycleCount), // Range (x.x-y.y, x.x.x-y.y.y) - VersionSpecKind.Range => GenerateRangeAvailabilityText(lifecycle, minVersion, maxVersion!, isMinReleased, isMaxReleased), + VersionSpecKind.Range => GenerateRangeAvailabilityText(lifecycle, minVersion, maxVersion!, isMinReleased, isMaxReleased, lifecycleCount), // Exact (=x.x, =x.x.x) - VersionSpecKind.Exact => GenerateExactAvailabilityText(lifecycle, minVersion, isMinReleased), + VersionSpecKind.Exact => GenerateExactAvailabilityText(lifecycle, minVersion, isMinReleased, lifecycleCount), _ => null }; @@ -262,20 +262,17 @@ private static PopoverData BuildPopoverData( return lifecycle switch { ProductLifecycle.Removed => $"Removed in {version}", - ProductLifecycle.Unavailable => $"Unavailable since {version}", _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} since {version}" }; } - // Unreleased - show the version so users see the full timeline in the popover - // Only hide Unavailable when there are other lifecycles (it's implied by other entries) return lifecycle switch { - ProductLifecycle.Deprecated => $"Deprecated since {version}", - ProductLifecycle.Removed => $"Removed in {version}", + ProductLifecycle.Deprecated => "Planned for deprecation", + ProductLifecycle.Removed => "Planned for removal", ProductLifecycle.Unavailable when lifecycleCount == 1 => "Unavailable", - ProductLifecycle.Unavailable => null, - _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} since {version}" + _ when lifecycleCount >= 2 => null, // Do not add to availability list + _ => "Planned" }; } @@ -283,7 +280,7 @@ private static PopoverData BuildPopoverData( /// Generates availability text for range version type. ///
private static string? GenerateRangeAvailabilityText( - ProductLifecycle lifecycle, string minVersion, string maxVersion, bool isMinReleased, bool isMaxReleased) + ProductLifecycle lifecycle, string minVersion, string maxVersion, bool isMinReleased, bool isMaxReleased, int lifecycleCount) { if (isMaxReleased) { @@ -306,20 +303,21 @@ private static PopoverData BuildPopoverData( }; } - // Neither released - show the version so users see the full timeline in the popover + // Neither released return lifecycle switch { - ProductLifecycle.Deprecated => $"Deprecated since {minVersion}", - ProductLifecycle.Removed => $"Removed in {minVersion}", + ProductLifecycle.Deprecated => "Planned for deprecation", + ProductLifecycle.Removed => "Planned for removal", ProductLifecycle.Unavailable => null, - _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} since {minVersion}" + _ when lifecycleCount >= 2 => null, // Do not add to availability list + _ => "Planned" }; } /// /// Generates availability text for exact version type. /// - private static string? GenerateExactAvailabilityText(ProductLifecycle lifecycle, string version, bool isReleased) + private static string? GenerateExactAvailabilityText(ProductLifecycle lifecycle, string version, bool isReleased, int lifecycleCount) { if (isReleased) { @@ -331,13 +329,14 @@ private static PopoverData BuildPopoverData( }; } - // Unreleased - show the version so users see the full timeline in the popover + // Unreleased return lifecycle switch { - ProductLifecycle.Deprecated => $"Deprecated in {version}", - ProductLifecycle.Removed => $"Removed in {version}", + ProductLifecycle.Deprecated => "Planned for deprecation", + ProductLifecycle.Removed => "Planned for removal", ProductLifecycle.Unavailable => null, - _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} in {version}" + _ when lifecycleCount >= 2 => null, // Do not add to availability list + _ => "Planned" }; } From 4c7bd53c1d9096524acf47569e376e2af0bcb2d0 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Tue, 16 Dec 2025 08:06:29 -0300 Subject: [PATCH 46/61] Adjust tests --- docs/testing/req.md | 4 +- .../Applicability/ApplicableToComponent.fs | 78 +++++++++---------- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index 76d8560a3..67a04ab17 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -127,4 +127,6 @@ The tutorial assumes that you have no previous knowledge of Elasticsearch or gen {applies_to}`stack: deprecated 7.16.0, removed 8.0.0` -{applies_to}`ess: ` \ No newline at end of file +{applies_to}`ess: ` + +{applies_to}`stack: preview 9.0, ga 9.2, deprecated 9.7` \ No newline at end of file diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index 244d917b5..0f1befe12 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -31,7 +31,7 @@ stack: ga 9.0.0 let ``renders GA with version`` () = markdown |> convertsToHtml """

- +

""" @@ -47,7 +47,7 @@ stack: preview 9.1.0 let ``renders preview future version as planned`` () = markdown |> convertsToHtml """

- +

""" @@ -63,7 +63,7 @@ stack: beta 8.8.0 let ``renders beta future version`` () = markdown |> convertsToHtml """

- +

""" @@ -79,7 +79,7 @@ stack: deprecated 8.7.0 let ``renders deprecation planned`` () = markdown |> convertsToHtml """

- +

""" @@ -95,7 +95,7 @@ stack: removed 8.6.0 let ``renders planned for removal`` () = markdown |> convertsToHtml """

- +

""" @@ -128,7 +128,7 @@ serverless: ga 9.0.0 let ``renders serverless ga planned`` () = markdown |> convertsToHtml """

- +

""" @@ -147,11 +147,11 @@ serverless: let ``renders serverless individual projects`` () = markdown |> convertsToHtml """

- + - + - +

""" @@ -171,7 +171,7 @@ deployment:

@@ -189,7 +189,7 @@ deployment: let ``renders ECK deployment`` () = markdown |> convertsToHtml """

- +

""" @@ -206,7 +206,7 @@ deployment: let ``renders ECH deployment`` () = markdown |> convertsToHtml """

- +

""" @@ -222,7 +222,7 @@ deployment: let ``renders self-managed deployment`` () = markdown |> convertsToHtml """

- +

""" @@ -243,17 +243,17 @@ apm_agent_python: preview 9.2.0

@@ -272,11 +272,11 @@ edot_python: preview 9.2.0 let ``renders EDOT agents planned`` () = markdown |> convertsToHtml """

- + - + - +

""" @@ -293,7 +293,7 @@ stack: ga 8.8.0, preview 8.1.0 let ``renders Planned when GA and Preview are both unreleased`` () = markdown |> convertsToHtml """

- +

""" @@ -309,7 +309,7 @@ stack: deprecated 9.1.0 let ``renders deprecation planned for future version`` () = markdown |> convertsToHtml """

- +

""" @@ -325,7 +325,7 @@ stack: removed 9.1.0 let ``renders removal planned for future version`` () = markdown |> convertsToHtml """

- +

""" @@ -400,19 +400,19 @@ apm_agent_java: beta 9.1.0 let ``renders complex mixed scenario`` () = markdown |> convertsToHtml """

- + - + - + - + - + - + - +

""" @@ -430,9 +430,9 @@ deployment: let ``renders stack and ece planned`` () = markdown |> convertsToHtml """

- + - +

""" @@ -472,19 +472,19 @@ product: ga 9.0.0 let ``renders VersioningSystemId coverage`` () = markdown |> convertsToHtml """

- + - + - + - + - + - + - +

""" @@ -502,7 +502,7 @@ stack: ga 8.0.0, beta 8.1.0 let ``renders multiple lifecycles with ellipsis and shows GA lifecycle`` () = markdown |> convertsToHtml """

- +

""" From 40a3e59520636dfd51ab5fe4fded63cbbe22d41d Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 03:50:59 -0300 Subject: [PATCH 47/61] Remove duplicate information --- .../AppliesTo/ApplicabilitySelector.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs index 483021d98..599d8f396 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs @@ -17,18 +17,6 @@ public static class ApplicabilitySelector /// The most relevant applicability for display public static Applicability GetPrimaryApplicability(IReadOnlyCollection applicabilities, SemVersion currentVersion) { - //var applicabilityList = applicabilities.ToList(); - var lifecycleOrder = new Dictionary - { - [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 AllVersionsSpec || a.Version.Min <= currentVersion).ToArray(); @@ -36,7 +24,7 @@ public static Applicability GetPrimaryApplicability(IReadOnlyCollection a.Version?.Min ?? new SemVersion(0, 0, 0)) - .ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999)) + .ThenBy(a => ProductLifecycleInfo.GetOrder(a.Lifecycle)) .First(); } @@ -47,7 +35,7 @@ public static Applicability GetPrimaryApplicability(IReadOnlyCollection a.Version!.Min.CompareTo(currentVersion)) - .ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999)) + .ThenBy(a => ProductLifecycleInfo.GetOrder(a.Lifecycle)) .First(); } From 590694783c6a6337efe36eb15f4d4a247db6e853 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 03:51:27 -0300 Subject: [PATCH 48/61] Fix badge display ordering --- docs/testing/req.md | 4 +++- .../Myst/Components/ApplicabilityRenderer.cs | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index 67a04ab17..5405971ae 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -129,4 +129,6 @@ The tutorial assumes that you have no previous knowledge of Elasticsearch or gen {applies_to}`ess: ` -{applies_to}`stack: preview 9.0, ga 9.2, deprecated 9.7` \ No newline at end of file +{applies_to}`stack: preview 9.0, ga 9.2, deprecated 9.7` + +{applies_to}`stack: preview 9.0, removed 9.1` diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 7ffd0471f..96783b14c 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -48,10 +48,10 @@ public static ApplicabilityRenderData RenderApplicability( { var allApplications = new AppliesCollection([.. applicabilities]); - // Sort by lifecycle priority (GA > Beta > Preview > etc.) to determine display order + // Sort by version (highest first), then by lifecycle priority as tiebreaker var sortedApplicabilities = applicabilities - .OrderBy(a => ProductLifecycleInfo.GetOrder(a.Lifecycle)) - .ThenByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) + .OrderByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) + .ThenBy(a => ProductLifecycleInfo.GetOrder(a.Lifecycle)) .ToList(); // Find the first lifecycle that returns displayable badge data (non-empty text or version) From ae1186ac1ab32d336743cce2a75d865954245415 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 03:54:28 -0300 Subject: [PATCH 49/61] Temporary downgrade the severity of new warnings --- .../AppliesTo/ApplicableToYamlConverter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs index 208291f9d..b3bb76261 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs @@ -279,7 +279,7 @@ private static void ValidateApplicabilityCollection(string key, AppliesCollectio if (lifecyclesWithMultipleVersions.Count > 0) { var lifecycleNames = string.Join(", ", lifecyclesWithMultipleVersions); - diagnostics.Add((Severity.Warning, + diagnostics.Add((Severity.Hint, // Temporary downgrade to Hint until the currently available docs are adjusted $"Key '{key}': Multiple version declarations found for lifecycle(s): {lifecycleNames}. Only one version per lifecycle is allowed.")); } @@ -290,7 +290,7 @@ private static void ValidateApplicabilityCollection(string key, AppliesCollectio if (greaterThanItems.Count > 1) { - diagnostics.Add((Severity.Warning, + diagnostics.Add((Severity.Hint, // Temporary downgrade to Hint until the currently available docs are adjusted $"Key '{key}': Multiple items use greater-than-or-equal syntax. Only one item per key can use this syntax.")); } @@ -303,7 +303,7 @@ private static void ValidateApplicabilityCollection(string key, AppliesCollectio { var rangeDescriptions = invalidRanges.Select(item => $"{item.Lifecycle} ({item.Version!.Min.Major}.{item.Version.Min.Minor}-{item.Version.Max!.Major}.{item.Version.Max.Minor})"); - diagnostics.Add((Severity.Warning, + diagnostics.Add((Severity.Hint, // Temporary downgrade to Hint until the currently available docs are adjusted $"Key '{key}': Invalid range(s) where first version is greater than last version: {string.Join(", ", rangeDescriptions)}.")); } @@ -324,7 +324,7 @@ private static void ValidateApplicabilityCollection(string key, AppliesCollectio if (hasOverlaps) { - diagnostics.Add((Severity.Warning, + diagnostics.Add((Severity.Hint, // Temporary downgrade to Hint until the currently available docs are adjusted $"Key '{key}': Overlapping version ranges detected. Ensure version ranges do not overlap within the same key.")); } } From f4cd2de8554c34c16259d5b02dd512684b964c44 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 04:04:45 -0300 Subject: [PATCH 50/61] Remove a few instantiations in favor of static definitions --- .../Versions/VersionInference.cs | 4 ++-- src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs | 2 +- .../AppliesTo/ApplicableToYamlConverter.cs | 4 ++-- src/Elastic.Documentation/SemVersion.cs | 5 +++++ .../Myst/Components/ApplicabilityRenderer.cs | 4 ++-- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Elastic.Documentation.Configuration/Versions/VersionInference.cs b/src/Elastic.Documentation.Configuration/Versions/VersionInference.cs index 102dca32f..08259d8ae 100644 --- a/src/Elastic.Documentation.Configuration/Versions/VersionInference.cs +++ b/src/Elastic.Documentation.Configuration/Versions/VersionInference.cs @@ -96,7 +96,7 @@ public class NoopVersionInferrer : IVersionInferrerService public VersioningSystem InferVersion(string repositoryName, IReadOnlyCollection? legacyPages, IReadOnlyCollection? products, ApplicableTo? applicableTo) => new() { Id = VersioningSystemId.Stack, - Base = new SemVersion(0, 0, 0), - Current = new SemVersion(0, 0, 0) + Base = ZeroVersion.Instance, + Current = ZeroVersion.Instance }; } diff --git a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs index 599d8f396..eb4a0f7f3 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs @@ -23,7 +23,7 @@ public static Applicability GetPrimaryApplicability(IReadOnlyCollection 0) { return availableApplicabilities - .OrderByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) + .OrderByDescending(a => a.Version?.Min ?? ZeroVersion.Instance) .ThenBy(a => ProductLifecycleInfo.GetOrder(a.Lifecycle)) .First(); } diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs index b3bb76261..b22c59ed4 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs @@ -347,8 +347,8 @@ private static bool CheckVersionOverlap(VersionSpec v1, VersionSpec v2) var (v1Min, v1Max) = GetEffectiveRange(v1); var (v2Min, v2Max) = GetEffectiveRange(v2); - return v1Min.CompareTo(v2Max ?? new SemVersion(99999, 0, 0)) <= 0 && - v2Min.CompareTo(v1Max ?? new SemVersion(99999, 0, 0)) <= 0; + return v1Min.CompareTo(v2Max ?? AllVersions.Instance) <= 0 && + v2Min.CompareTo(v1Max ?? AllVersions.Instance) <= 0; } private static (SemVersion min, SemVersion? max) GetEffectiveRange(VersionSpec spec) => spec.Kind switch diff --git a/src/Elastic.Documentation/SemVersion.cs b/src/Elastic.Documentation/SemVersion.cs index e1cb736da..6784bb484 100644 --- a/src/Elastic.Documentation/SemVersion.cs +++ b/src/Elastic.Documentation/SemVersion.cs @@ -13,6 +13,11 @@ public class AllVersions() : SemVersion(99999, 0, 0) public static AllVersions Instance { get; } = new(); } +public class ZeroVersion() : SemVersion(0, 0, 0) +{ + public static ZeroVersion Instance { get; } = new(); +} + /// /// A semver2 compatible version. /// diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 96783b14c..75dc2c969 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -50,7 +50,7 @@ public static ApplicabilityRenderData RenderApplicability( // Sort by version (highest first), then by lifecycle priority as tiebreaker var sortedApplicabilities = applicabilities - .OrderByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)) + .OrderByDescending(a => a.Version?.Min ?? ZeroVersion.Instance) .ThenBy(a => ProductLifecycleInfo.GetOrder(a.Lifecycle)) .ToList(); @@ -156,7 +156,7 @@ private static PopoverData BuildPopoverData( // Availability section - collect items from all applicabilities // Order by version descending (most recent/future first, then going backwards) var orderedApplicabilities = applicabilities - .OrderByDescending(a => a.Version?.Min ?? new SemVersion(0, 0, 0)); + .OrderByDescending(a => a.Version?.Min ?? ZeroVersion.Instance); var showVersionNote = productInfo is { IncludeVersionNote: true } && versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major; From a7a55e4042bcad27b15d888db73f4408230b33ee Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 04:20:14 -0300 Subject: [PATCH 51/61] Remove code duplications --- .../Versions/VersionConfiguration.cs | 2 ++ src/Elastic.Documentation/VersionSpec.cs | 4 +--- .../Myst/Components/ApplicabilityRenderer.cs | 21 +++++++------------ 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs b/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs index 3a4ab61f0..de122351f 100644 --- a/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs +++ b/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs @@ -151,4 +151,6 @@ public record VersioningSystem [YamlMember(Alias = "current")] public required SemVersion Current { get; init; } + + public bool IsVersioned() => Base.Major != AllVersions.Instance.Major; } diff --git a/src/Elastic.Documentation/VersionSpec.cs b/src/Elastic.Documentation/VersionSpec.cs index 34558ef65..85de1cff8 100644 --- a/src/Elastic.Documentation/VersionSpec.cs +++ b/src/Elastic.Documentation/VersionSpec.cs @@ -8,9 +8,7 @@ namespace Elastic.Documentation; public sealed class AllVersionsSpec : VersionSpec { - private static readonly SemVersion AllVersionsSemVersion = new(99999, 0, 0); - - private AllVersionsSpec() : base(AllVersionsSemVersion, null, VersionSpecKind.GreaterThanOrEqual) + private AllVersionsSpec() : base(AllVersions.Instance, null, VersionSpecKind.GreaterThanOrEqual) { } diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 75dc2c969..efd16cd1a 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -78,9 +78,8 @@ public static ApplicabilityRenderData RenderApplicability( } // If we've exhausted all options (none had displayable data), use the first one with "Planned" - // But only for versioned products - unversioned products (base=99999) should show empty badge - var isVersionedProduct = versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major; - if (badgeData is null && firstBadgeData is not null && isVersionedProduct) + // But only for versioned products - unversioned products should show empty badge + if (badgeData is null && firstBadgeData is not null && versioningSystem.IsVersioned()) badgeData = firstBadgeData with { BadgeLifecycleText = "Planned" }; badgeData ??= GetBadgeData(sortedApplicabilities.First(), versioningSystem, allApplications); @@ -158,8 +157,7 @@ private static PopoverData BuildPopoverData( var orderedApplicabilities = applicabilities .OrderByDescending(a => a.Version?.Min ?? ZeroVersion.Instance); - var showVersionNote = productInfo is { IncludeVersionNote: true } && - versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major; + var showVersionNote = productInfo is { IncludeVersionNote: true } && versioningSystem.IsVersioned(); return new PopoverData( ProductDescription: productInfo?.Description, @@ -210,16 +208,11 @@ private static PopoverData BuildPopoverData( var lifecycle = applicability.Lifecycle; var versionSpec = applicability.Version; - // No version (null or AllVersionsSpec) with unversioned product - if ((versionSpec is null || versionSpec is AllVersionsSpec) && - versioningSystem.Base.Major == AllVersionsSpec.Instance.Min.Major) - { - return ProductLifecycleInfo.GetDisplayText(lifecycle); - } - - // No version with versioned product if (versionSpec is null or AllVersionsSpec) { + if (!versioningSystem.IsVersioned()) + return ProductLifecycleInfo.GetDisplayText(lifecycle); + var baseVersion = $"{versioningSystem.Base.Major}.{versioningSystem.Base.Minor}"; return lifecycle switch { @@ -419,7 +412,7 @@ private static string GetBadgeVersionText(VersionSpec? versionSpec, VersioningSy case AllVersionsSpec: case null: // Only show base version if the product is versioned - return versioningSystem.Base.Major != AllVersionsSpec.Instance.Min.Major + return versioningSystem.IsVersioned() ? $"{versioningSystem.Base.Major}.{versioningSystem.Base.Minor}+" : string.Empty; default: From 86fce10db86ec38a3faafdf9cbadd8f79ab24e30 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 04:39:14 -0300 Subject: [PATCH 52/61] Fix tests --- src/Elastic.Documentation/AppliesTo/Applicability.cs | 2 +- tests/authoring/Inline/AppliesToRole.fs | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Elastic.Documentation/AppliesTo/Applicability.cs b/src/Elastic.Documentation/AppliesTo/Applicability.cs index a2d1149a3..2164a485e 100644 --- a/src/Elastic.Documentation/AppliesTo/Applicability.cs +++ b/src/Elastic.Documentation/AppliesTo/Applicability.cs @@ -109,7 +109,7 @@ private static List InferVersionSemantics(List app // Apply the mapping to create updated applications return applications.Select(a => { - if (a.Version is null || a.Version == AllVersionsSpec.Instance || a is not { Version.Kind: VersionSpecKind.GreaterThanOrEqual }) + if (a.Version is null or AllVersionsSpec || a is not { Version.Kind: VersionSpecKind.GreaterThanOrEqual }) return a; if (versionMapping.TryGetValue(a.Version.Min, out var newSpec)) diff --git a/tests/authoring/Inline/AppliesToRole.fs b/tests/authoring/Inline/AppliesToRole.fs index 12cd244d3..5600c6a15 100644 --- a/tests/authoring/Inline/AppliesToRole.fs +++ b/tests/authoring/Inline/AppliesToRole.fs @@ -30,10 +30,7 @@ This is an inline {applies_to}`stack: preview 9.1` element. markdown |> convertsToHtml """

This is an inline - + element.

@@ -137,11 +134,8 @@ This is an inline {applies_to}`stack: preview 8.0, ga 8.1` element. markdown |> convertsToHtml """

This is an inline - - + + element.

""" From 2724eb727546a1b80c2c38cc22990efd26e008ec Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 05:01:38 -0300 Subject: [PATCH 53/61] Fix range rendering when the difference is at the patch level --- src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index efd16cd1a..928af22d3 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -431,7 +431,9 @@ private static string GetBadgeVersionText(VersionSpec? versionSpec, VersioningSy : string.Empty, VersionSpecKind.Range => maxReleased - ? $"{min.Major}.{min.Minor}-{max!.Major}.{max.Minor}" + ? min.Major == max!.Major && min.Minor == max.Minor + ? $"{min.Major}.{min.Minor}" // Same major.minor, so just show the version once + : $"{min.Major}.{min.Minor}-{max.Major}.{max.Minor}" : minReleased ? $"{min.Major}.{min.Minor}+" : string.Empty, From c4b6fd4afa647beae753030f4f6389ebaab2e8e7 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 05:14:20 -0300 Subject: [PATCH 54/61] Show range-level versions on applicability items --- docs/testing/req.md | 2 ++ .../Myst/Components/ApplicabilityRenderer.cs | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index 5405971ae..537bbf88d 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -132,3 +132,5 @@ The tutorial assumes that you have no previous knowledge of Elasticsearch or gen {applies_to}`stack: preview 9.0, ga 9.2, deprecated 9.7` {applies_to}`stack: preview 9.0, removed 9.1` + +{applies_to}`stack: preview 9.0.0-9.0.3, removed 9.3` diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 928af22d3..3ff2623d2 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -225,8 +225,13 @@ private static PopoverData BuildPopoverData( // Get version info var min = versionSpec.Min; var max = versionSpec.Max; - var minVersion = $"{min.Major}.{min.Minor}"; - var maxVersion = max is not null ? $"{max.Major}.{max.Minor}" : null; + var isPatchLevelRange = max is not null && min.Major == max.Major && min.Minor == max.Minor; + var minVersion = isPatchLevelRange ? min.ToString() : $"{min.Major}.{min.Minor}"; + var maxVersion = max is not null + ? isPatchLevelRange + ? max.ToString() + : $"{max.Major}.{max.Minor}" + : null; var isMinReleased = min <= versioningSystem.Current; var isMaxReleased = max is not null && max <= versioningSystem.Current; From 0cf82bb39d780b9b6ee72f452c190e7685f2d9e3 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 05:18:39 -0300 Subject: [PATCH 55/61] Update src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx Co-authored-by: florent-leborgne --- .../Assets/web-components/AppliesToPopover.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx index b39b9a1e6..00675b36f 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx @@ -417,7 +417,6 @@ const AppliesToPopover = ({ font-size: 13px; `} > - The functionality described here is:

{popoverData.availabilityItems.map((item, index) => renderAvailabilityItem(item, index) From a7f0364a0f79210f0e72907984191df256b3c850 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 05:19:50 -0300 Subject: [PATCH 56/61] Update src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs Co-authored-by: Visha Angelova <91186315+vishaangelova@users.noreply.github.com> --- src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs b/src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs index 736233d6f..906c8482f 100644 --- a/src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs +++ b/src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs @@ -67,7 +67,7 @@ bool IncludeVersionNote // Deployment Types [VersioningSystemId.Ess] = new ProductInfo( - Description: "Elastic Cloud Hosted is a deployment of the Elastic Stack that's hosted on Elastic Cloud.", + Description: "Elastic Cloud Hosted lets you manage and configure one or more deployments of the versioned Elastic Stack, hosted on Elastic Cloud.", AdditionalAvailabilityInfo: null, IncludeVersionNote: false ), From da72ba00d5e95546a22dee7a5c2ae7ef924d42a1 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 05:24:01 -0300 Subject: [PATCH 57/61] Fix tests --- tests/authoring/Applicability/ApplicableToComponent.fs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index 0f1befe12..eddfdda62 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -206,8 +206,9 @@ deployment: let ``renders ECH deployment`` () = markdown |> convertsToHtml """

- -

+ + +

""" type ``deployment self managed`` () = @@ -476,7 +477,7 @@ product: ga 9.0.0 - + From 7d8546ceb935e306306863a322c0d450a45c8fb0 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 05:28:10 -0300 Subject: [PATCH 58/61] lint fix --- .../Assets/web-components/AppliesToPopover.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx index 00675b36f..c5918c61a 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx @@ -416,8 +416,7 @@ const AppliesToPopover = ({ color: var(--color-grey-70, #535966); font-size: 13px; `} - > -

+ >

{popoverData.availabilityItems.map((item, index) => renderAvailabilityItem(item, index) )} From e293bd6e5af833ac82932601159e789889951f80 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 10:11:52 -0300 Subject: [PATCH 59/61] Revert showing patch-level versions in applicability headers --- docs/testing/req.md | 4 ++++ .../Myst/Components/ApplicabilityRenderer.cs | 11 +++-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/testing/req.md b/docs/testing/req.md index 537bbf88d..a9cd202d0 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -134,3 +134,7 @@ The tutorial assumes that you have no previous knowledge of Elasticsearch or gen {applies_to}`stack: preview 9.0, removed 9.1` {applies_to}`stack: preview 9.0.0-9.0.3, removed 9.3` + +{applies_to}`stack: preview 9.0, ga 9.4, removed 9.7` + +{applies_to}`stack: preview 9.0, deprecated 9.4, removed 9.7` diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 3ff2623d2..57ce08df1 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -114,7 +114,7 @@ private static BadgeData GetBadgeData( var showLifecycle = applicability.Lifecycle != ProductLifecycle.GenerallyAvailable && string.IsNullOrEmpty(badgeLifecycleText); - // Determine if we should show version based on VersionSpec + // Determine if we should show the version based on VersionSpec var versionDisplay = GetBadgeVersionText(applicability.Version, versioningSystem); var showVersion = !string.IsNullOrEmpty(versionDisplay); @@ -225,13 +225,8 @@ private static PopoverData BuildPopoverData( // Get version info var min = versionSpec.Min; var max = versionSpec.Max; - var isPatchLevelRange = max is not null && min.Major == max.Major && min.Minor == max.Minor; - var minVersion = isPatchLevelRange ? min.ToString() : $"{min.Major}.{min.Minor}"; - var maxVersion = max is not null - ? isPatchLevelRange - ? max.ToString() - : $"{max.Major}.{max.Minor}" - : null; + var minVersion = $"{min.Major}.{min.Minor}"; + var maxVersion = max is not null ? $"{max.Major}.{max.Minor}" : null; var isMinReleased = min <= versioningSystem.Current; var isMaxReleased = max is not null && max <= versioningSystem.Current; From 34e97a67e154bf355b631b06d40ad26d21201837 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 10:51:28 -0300 Subject: [PATCH 60/61] Add applicability ruleset tables to the docs --- docs/syntax/applies.md | 205 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 1 deletion(-) diff --git a/docs/syntax/applies.md b/docs/syntax/applies.md index 59c33d720..49d5745c0 100644 --- a/docs/syntax/applies.md +++ b/docs/syntax/applies.md @@ -406,4 +406,207 @@ Within the ProductApplicability category, EDOT and APM Agent items are sorted al :::{note} Inline applies annotations are rendered in the order they appear in the source file. -::: \ No newline at end of file +::: + +## Rulesets + +Badges and applicabilities are displayed according to pre-defined rules according to the release status, the amount of lifecycles declared in the `applies_to` statement, and the versions involved in the comparison when applicable. + +### Badges + +:::::{dropdown} No version declared (Serverless) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | – | – | `{product}` | +| Preview | – | – | `{product}\|Preview` | +| Beta | – | – | `{product} |Beta` | +| Deprecated | – | – | `{product} |Deprecated` | +| Removed | – | – | `{product} |Removed` | +| Unavailable | – | – | `{product} |Unavailable` | + +::::: + +:::::{dropdown} No version declared (Other versioning systems) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | – | – | `{product} |{base}+` | +| Preview | – | – | `{product}\|Preview {base}+` | +| Beta | – | – | `{product} |Beta {base}+` | +| Deprecated | – | – | `{product} |Deprecated {base}+` | +| Removed | – | – | `{product} |Removed {base}+` | +| Unavailable | – | – | `{product} |Unavailable {base}+` | + +::::: + +:::::{dropdown} Greater than or equal to "x.x" (x.x+, x.x, x.x.x+, x.x.x) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | Released | \>= 1 | `{product} |x.x+` | +| | Unreleased | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Preview | Released | \>= 1 | `{product}\|Preview x.x+` | +| | Unreleased | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Beta | Released | \>= 1 | `{product} |Beta x.x+` | +| | Unreleased | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Deprecated | Released | \>= 1 | `{product} |Deprecated x.x+` | +| | Unreleased | 1 | `{product} |Deprecation planned` | +| | | \>= 2 | Use previous lifecycle | +| Removed | Released | \>= 1 | `{product} |Removed x.x` | +| | Unreleased | 1 | `{product} |Removal planned` | +| | | \>= 2 | Use previous lifecycle | + +::::: + +:::::{dropdown} Range of "x.x-y.y" (x.x-y.y, x.x.x-y.y.y) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | `y.y.y` is released | \>= 1 | `{product} |x.x-y.y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `{product} |x.x+` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Preview | `y.y.y` is released | \>= 1 | `{product}\|Preview x.x-y.y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `{product}\|Preview x.x+` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Beta | `y.y.y` is released | \>= 1 | `{product} |Beta x.x-y.y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `{product} |Beta x.x+` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Deprecated | `y.y.y` is released | \>= 1 | `{product} |Deprecated x.x-y.y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `{product} |Deprecated x.x+` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | \>= 1 | `{product} |Deprecation planned` | +| Removed | `y.y.y` is released | \>= 1 | `{product} |Removed x.x` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `{product} |Removed x.x` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | \>= 1 | `{product} |Removal planned` | +| Unavailable | `y.y.y` is released | \>= 1 | `{product} |Unavailable X.X-Y.Y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `{product} |Unavailable X.X+` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | \>= 1 | ??? | + +::::: + +:::::{dropdown} Exactly "x.x" (=x.x, =x.x.x) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | Released | \>= 1 | `{product} |X.X` | +| | Unreleased | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Preview | Released | \>= 1 | `{product}\|Preview X.X` | +| | Unreleased | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Beta | Released | \>= 1 | `{product} |Beta X.X` | +| | Unreleased | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Deprecated | Released | \>= 1 | `{product} |Deprecated X.X` | +| | Unreleased | \>= 1 | `{product} |Deprecation planned` | +| Removed | Released | \>= 1 | `{product} |Removed X.X` | +| | Unreleased | \>=1 | `{product} |Removal planned` | +| Unavailable | Released | \>= 1 | `{product} |Unavailable X.X` | +| | Unreleased | \>= 1 | ??? | + +::::: + +### Headers for dynamic content on popover, based on lifecycle/version (Applicability list) + +:::::{dropdown} No version declared (Serverless) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | – | 1 | `Generally available` | +| Preview | – | 1 | `Preview` | +| Beta | – | 1 | `Beta` | +| Deprecated | – | 1 | `Deprecated` | +| Removed | – | 1 | `Removed` | +| Unavailable | – | 1 | `Unavailable` | +::::: + +:::::{dropdown} No version declared (Other versioning systems) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | – | 1 | `Generally available since {base}` | +| Preview | – | 1 | `Preview since {base}` | +| Beta | – | 1 | `Beta since {base}` | +| Deprecated | – | 1 | `Deprecated since {base}` | +| Removed | – | 1 | `Removed in {base}` | +| Unavailable | – | 1 | `Unavailable since {base}` | +::::: + +:::::{dropdown} Greater than or equal to "x.x" (x.x+, x.x, x.x.x+, x.x.x) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | Released | \>= 1 | `Generally available since X.X` | +| | Unreleased | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Preview | Released | \>= 1 | `Preview since X.X` | +| | Unreleased | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Beta | Released | \>= 1 | `Beta since X.X` | +| | Unreleased | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Deprecated | Released | \>= 1 | `Deprecated since X.X` | +| | Unreleased | \>= 1 | `Planned for deprecation` | +| Removed | Released | \>= 1 | `Removed in X.X` | +| | Unreleased | \>=1 | `Planned for removal` | +| Unavailable | Released | \>= 1 | `Unavailable since X.X` | +| | Unreleased | 1 | `Unavailable` | +| | | \>= 2 | Do not add to availability list | +::::: + +:::::{dropdown} Range of "x.x-y.y" (x.x-y.y, x.x.x-y.y.y) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | `y.y.y` is released | \>= 1 | `Generally available from X.X to Y.Y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `Generally available since X.X` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Preview | `y.y.y` is released | \>= 1 | `Preview from X.X to Y.Y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `Preview since X.X` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Beta | `y.y.y` is released | \>= 1 | `Beta from X.X to Y.Y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `Beta since X.X` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Deprecated | `y.y.y` is released | \>= 1 | `Deprecated from X.X to Y.Y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `Deprecated since X.X` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | \>= 1 | `Planned for deprecation` | +| Removed | `y.y.y` is released | \>= 1 | `Removed in X.X` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `Removed in X.X` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | \>= 1 | `Planned for removal` | +| Unavailable | `y.y.y` is released | \>= 1 | `Unavailable from X.X to Y.Y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `Unavailable since X.X` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | \>= 1 | Do not add to availability list | + + +::::: + +:::::{dropdown} Exactly "x.x" (=x.x, =x.x.x) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | Released | \>= 1 | `Generally available in X.X` | +| | Unreleased | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Preview | Released | \>= 1 | `Preview in X.X` | +| | Unreleased | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Beta | Released | \>= 1 | `Beta in X.X` | +| | Unreleased | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Deprecated | Released | \>= 1 | `Deprecated in X.X` | +| | Unreleased | \>= 1 | `Planned for deprecation` | +| Removed | Released | \>= 1 | `Removed in X.X` | +| | Unreleased | \>=1 | `Planned for removal` | +| Unavailable | Released | \>= 1 | `Unavailable in X.X` | +| | Unreleased | \>= 1 | Do not add to availability list | + +::::: From d45bcf9b56fc57bd28309e5d05290569feebcafa Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Wed, 17 Dec 2025 10:53:21 -0300 Subject: [PATCH 61/61] Remove redundant snippet --- docs/syntax/applies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/syntax/applies.md b/docs/syntax/applies.md index 49d5745c0..a1b990865 100644 --- a/docs/syntax/applies.md +++ b/docs/syntax/applies.md @@ -512,7 +512,7 @@ Badges and applicabilities are displayed according to pre-defined rules accordin ::::: -### Headers for dynamic content on popover, based on lifecycle/version (Applicability list) +### Headers for dynamic content on popover (Applicability list) :::::{dropdown} No version declared (Serverless)