diff --git a/preview-src/property-tooltips-test.adoc b/preview-src/property-tooltips-test.adoc
index 9fd7df5c..3f0d3374 100644
--- a/preview-src/property-tooltips-test.adoc
+++ b/preview-src/property-tooltips-test.adoc
@@ -1,29 +1,29 @@
= Property Tooltips Test
:page-layout: default
-Test page for Redpanda configuration property tooltips. Property tooltips are enabled by default on all pages. To disable on a specific page, use `:page-disable-property-tooltips: true`.
+Test page for Redpanda configuration property tooltips. Tooltips are opt-in: only code elements emitted by the `prop:` AsciiDoc macro (class `property-ref` plus a `data-property-name` attribute) are decorated. This page simulates the macro's output with pass-through HTML because the UI preview doesn't run the docs macros. To disable tooltips on a specific page, use `:page-disable-property-tooltips: true`.
TIP: Hover over property names to see documentation tooltips!
== Cluster Properties in Paragraphs
-When configuring Redpanda, you may need to adjust `log_segment_size` for optimal storage performance. The default segment size works for most workloads, but large messages may benefit from larger segments.
+When configuring Redpanda, you may need to adjust +++log_segment_size+++ for optimal storage performance. The default segment size works for most workloads, but large messages may benefit from larger segments.
-To enable authentication, set `enable_sasl` to true. This requires clients to authenticate using SASL mechanisms.
+To enable authentication, set +++enable_sasl+++ to true. This requires clients to authenticate using SASL mechanisms.
== Topic Properties in Lists
Topic-level configuration properties:
-* `retention_bytes` - Controls maximum partition size before deletion
+* +++retention_bytes+++ - Controls maximum partition size before deletion
* This is a topic-scoped property that can override cluster defaults
== Dotted Property Names (Topic-scoped)
These properties use dot notation and test the anchor slugification fix:
-* `redpanda.storage.mode` - Controls storage mode (local vs tiered)
-* `redpanda.remote.read` - Enables remote reads from object storage
+* +++redpanda.storage.mode+++ - Controls storage mode (local vs tiered)
+* +++redpanda.remote.read+++ - Enables remote reads from object storage
The "View full documentation" link for `redpanda.storage.mode` should generate an anchor like `#redpanda-storage-mode` (dots replaced with hyphens), not `#redpanda.storage.mode`.
@@ -31,15 +31,35 @@ The "View full documentation" link for `redpanda.storage.mode` should generate a
Some properties are enterprise-only:
-* `cloud_storage_enabled` - Requires Enterprise license (self-hosted only)
+* +++cloud_storage_enabled+++ - Requires Enterprise license (self-hosted only)
== Broker Properties
-Per-broker settings like `kafka_connections_max` control resource limits.
+Per-broker settings like +++kafka_connections_max+++ control resource limits.
+
+== Macro Calls Inside Tooltip Descriptions
+
+The +++group_max_session_timeout_ms+++ tooltip description contains a `prop:` macro call and a legacy `config_ref:` call. Both must render as plain code in the tooltip, never as raw macro text.
== Deprecated Properties
-The property `group_max_session_timeout_ms` is deprecated but still shown with appropriate styling.
+The property +++group_max_session_timeout_ms+++ is deprecated but still shown with appropriate styling.
+
+== Display text override
+
+The macro's text attribute changes the display while keeping the lookup name: +++object storage flag+++ should show the Tiered Storage property tooltip.
+
+== Unmarked property names get NO tooltip (the old false-positive case)
+
+These are real property names in plain backticks, exactly what used to be auto-matched. With opt-in marking they must NOT have tooltips:
+
+* `log_segment_size` - No tooltip (not marked)
+* `enable_sasl` - No tooltip (not marked)
+* The `admin` listener in a Helm values file - No tooltip (ambiguous word)
+
+== Marked but unknown property
+
+A marked element whose name is not in the published data renders untouched: +++this_is_not_a_property+++ - No tooltip.
== Properties in Code Blocks
@@ -57,7 +77,3 @@ Regular code elements should NOT have tooltips:
* `this_is_not_a_property` - No tooltip
* `some_random_function()` - No tooltip
* `myVariable` - No tooltip
-
-== Mixed Content
-
-In a paragraph with both properties and regular text: Set `enable_sasl` to configure authentication, then use `kubectl apply` to deploy. Only `enable_sasl` should have a tooltip.
diff --git a/src/css/property-tooltips.css b/src/css/property-tooltips.css
index 6f1e2cc2..1793fac8 100644
--- a/src/css/property-tooltips.css
+++ b/src/css/property-tooltips.css
@@ -113,10 +113,64 @@ code.has-property-tooltip:focus-visible {
border: 1px solid #f5c6cb;
}
-/* Description text */
+/* Description text. Long generated descriptions scroll inside the tooltip
+ so the signature, default, and documentation link always stay visible.
+ The scrollbar is styled so it stays visible when content overflows:
+ macOS overlay scrollbars only appear mid-scroll, which hides the fact
+ that there is more content. Styling ::-webkit-scrollbar opts out of the
+ overlay behavior in Chrome and Safari. */
.property-doc-tooltip .prop-tooltip-description {
margin-bottom: 12px;
color: var(--body-font-color, #333);
+ max-height: 14em;
+ overflow-y: auto;
+ padding-right: 6px;
+ scrollbar-width: thin;
+ scrollbar-color: rgba(100, 100, 100, 0.5) transparent;
+}
+
+.property-doc-tooltip .prop-tooltip-description::-webkit-scrollbar {
+ width: 6px;
+}
+
+.property-doc-tooltip .prop-tooltip-description::-webkit-scrollbar-thumb {
+ background: rgba(100, 100, 100, 0.5);
+ border-radius: 3px;
+}
+
+.property-doc-tooltip .prop-tooltip-description::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+html[data-theme="dark"] .property-doc-tooltip .prop-tooltip-description::-webkit-scrollbar-thumb {
+ background: rgba(180, 180, 180, 0.5);
+}
+
+.property-doc-tooltip .prop-tooltip-description p {
+ margin: 0 0 8px;
+}
+
+.property-doc-tooltip .prop-tooltip-description p:last-child {
+ margin-bottom: 0;
+}
+
+.property-doc-tooltip .prop-tooltip-description ul {
+ margin: 0 0 8px;
+ padding-left: 18px;
+}
+
+.property-doc-tooltip .prop-tooltip-description li {
+ margin-bottom: 4px;
+}
+
+/* Links inside the rendered description (xref and <<...>> references) */
+.property-doc-tooltip .prop-tooltip-description a {
+ color: var(--link-color, #005cc5);
+ text-decoration: underline;
+}
+
+.property-doc-tooltip .prop-tooltip-description a:hover {
+ color: var(--link-color-hover, #004094);
}
.property-doc-tooltip .prop-tooltip-description code {
@@ -165,6 +219,7 @@ html[data-theme="dark"] .property-doc-tooltip .prop-tooltip-signature {
html[data-theme="dark"] .property-doc-tooltip .prop-tooltip-description {
color: #e0e0e0;
+ scrollbar-color: rgba(180, 180, 180, 0.5) transparent;
}
html[data-theme="dark"] .property-doc-tooltip .prop-tooltip-description code,
@@ -206,6 +261,14 @@ html[data-theme="dark"] .property-doc-tooltip .prop-tooltip-link:hover {
color: #79b8ff;
}
+html[data-theme="dark"] .property-doc-tooltip .prop-tooltip-description a {
+ color: #58a6ff;
+}
+
+html[data-theme="dark"] .property-doc-tooltip .prop-tooltip-description a:hover {
+ color: #79b8ff;
+}
+
/* Touch device support - larger tap targets */
@media (hover: none) and (pointer: coarse) {
code.has-property-tooltip {
diff --git a/src/helpers/resolve-resource.js b/src/helpers/resolve-resource.js
index 018ecf1f..46638c9f 100644
--- a/src/helpers/resolve-resource.js
+++ b/src/helpers/resolve-resource.js
@@ -42,12 +42,16 @@ function logUnresolved (resource, reason, page, context, logger) {
module.exports = (resource, { data, hash: context }) => {
const { page, logger } = data.root || {}
const fallbackUrl = context?.fallback
+ // An explicitly provided fallback (even an empty one) marks the resolution
+ // as optional: the caller handles the miss, so an unresolved target is
+ // expected on components that don't publish the resource and must not warn.
+ const optional = context ? 'fallback' in context : false
// Log and return undefined if resource is not provided
if (!resource || typeof resource !== 'string') {
// Only log if we have page context (not during initial template compilation)
if (page && resource === undefined) {
- logUnresolved('undefined', 'attribute not defined (check page attributes)', page, context, logger)
+ if (!optional) logUnresolved('undefined', 'attribute not defined (check page attributes)', page, context, logger)
}
return fallbackUrl || undefined
}
@@ -108,7 +112,21 @@ module.exports = (resource, { data, hash: context }) => {
}
if (page && page.component) {
- context = Object.assign({ component: page.component.name, version: page.version, module: page.module }, context)
+ // Hash params whose expression evaluated to undefined must not clobber
+ // the page defaults (version=(or ...) can legitimately come up empty).
+ const explicit = {}
+ for (const [key, value] of Object.entries(context || {})) {
+ if (value !== undefined) explicit[key] = value
+ }
+ context = Object.assign({ component: page.component.name, version: page.version, module: page.module }, explicit)
+ }
+
+ // Cross-component resolution from a versionless component (version '')
+ // asks for a version the target component doesn't have. Default to the
+ // target component's latest version instead.
+ if (!context.version && context.component && contentCatalog.getComponent) {
+ const targetComponent = contentCatalog.getComponent(context.component)
+ if (targetComponent && targetComponent.latest) context.version = targetComponent.latest.version
}
// Convert module-relative resource (module:page.adoc) to fully qualified (version@component:module:page.adoc)
@@ -141,7 +159,7 @@ module.exports = (resource, { data, hash: context }) => {
result = fallbackUrl
} else {
// Log warning for unresolved resource (only if no fallback provided)
- logUnresolved(resolvedResource, 'target not found in content catalog', page, context, logger)
+ if (!optional) logUnresolved(resolvedResource, 'target not found in content catalog', page, context, logger)
result = resource
}
diff --git a/src/js/16-bloblang-interactive.js b/src/js/16-bloblang-interactive.js
index 6aceb4fc..b7dbbde3 100644
--- a/src/js/16-bloblang-interactive.js
+++ b/src/js/16-bloblang-interactive.js
@@ -500,6 +500,15 @@
// Mouse devices: show on hover
trigger: isTouch ? 'click' : 'mouseenter focus',
hideOnClick: isTouch ? 'toggle' : true,
+ // Same show delay as the glossary and enterprise tooltips, so
+ // dragging the cursor across a code block doesn't fire previews.
+ delay: [200, 0],
+ popperOptions: {
+ modifiers: [
+ { name: 'preventOverflow', options: { boundary: 'viewport' } },
+ { name: 'flip', options: { fallbackPlacements: ['bottom', 'top'] } },
+ ],
+ },
onShow(instance) {
// Hide other tooltips
document.querySelectorAll('.tippy-box').forEach(box => {
diff --git a/src/js/19-property-tooltips.js b/src/js/19-property-tooltips.js
index 7efab8b0..d56cafd4 100644
--- a/src/js/19-property-tooltips.js
+++ b/src/js/19-property-tooltips.js
@@ -2,8 +2,12 @@
/**
* Redpanda Property Tooltips
*
- * Adds hover documentation tooltips to configuration property names.
- * Enabled by default on all pages. Disable on specific pages with:
+ * Adds hover documentation tooltips to configuration property references.
+ * Marking is opt-in: only code elements emitted by the prop: AsciiDoc macro
+ * (class property-ref plus a data-property-name attribute) are decorated.
+ * Plain backticked words are never matched, so ambiguous terms such as
+ * admin or rack in Helm or feature contexts don't pick up wrong tooltips.
+ * Disable on specific pages with:
* :page-disable-property-tooltips: true
*/
@@ -34,6 +38,21 @@
return null
}
+ /**
+ * Get the component-local property pages base URL from meta tag.
+ * head-meta resolves reference:properties/cluster-properties.adoc in the
+ * current page's own component, so cloud pages link to cloud's property
+ * pages, streaming pages to streaming's, and so on.
+ */
+ function getPropertiesPagesUrl () {
+ var meta = document.querySelector('meta[name="properties-pages-url"]')
+ // Ignore unresolved placeholders (the UI preview resolver emits '#').
+ if (meta && meta.content && meta.content.indexOf('cluster-properties') !== -1) {
+ return meta.content
+ }
+ return null
+ }
+
/**
* Get the latest Redpanda tag from meta tag (for cache versioning)
*/
@@ -255,9 +274,10 @@
parts.push('
tags
- * - Fallback xref resolution for any unresolved xrefs
+ * - prop:/config_ref macro calls rendered as code
+ * - <> internal references linked when they name a property
+ * - Fallback xref resolution for unqualified same-component targets
*/
- function formatDescription (text) {
+ function formatInline (text) {
if (!text) return ''
// Extract and preserve tags (already resolved in JSON generation)
@@ -329,22 +439,124 @@
// Convert backticks to code tags
var withCode = escaped.replace(/`([^`]+)`/g, '$1')
- // Fallback: resolve any remaining xrefs that weren't pre-resolved
- var withXrefs = withCode.replace(
- /xref:\.?\/?([^[]+)\.adoc(?:#([^[]*))?\[([^\]]+)\]/g,
+ // Render prop macro calls from generated descriptions as code (the
+ // text= attribute wins as the display, matching the macro's rendering)
+ var withProps = withCode.replace(/prop:([^[\s]+)\[([^\]]*)\]/g, function (match, name, attrs) {
+ var textMatch = attrs.match(/text=([^,\]]+)/)
+ return '' + (textMatch ? textMatch[1] : name) + ''
+ })
+
+ // Legacy config_ref macro calls survive in older published JSONs
+ withProps = withProps.replace(/config_ref:([^[,]+)(?:,[^[]*)?\[([^\]]*)\]/g, function (match, name, payload) {
+ var display = payload.replace(/^`|`$/g, '') || name
+ return '' + display + ''
+ })
+
+ // Internal <> references (escaped to <<...>>).
+ // Link when the anchor names another documented property; otherwise
+ // render the display text alone.
+ var withRefs = withProps.replace(/<<([^,&\s]+)(?:,\s*((?:(?!>>).)*?))?>>/g, function (match, anchor, display) {
+ var target = propertyForAnchor(anchor)
+ var label = display || (target ? '' + target.name + '' : anchor)
+ if (target) {
+ return '' + label + ''
+ }
+ return label
+ })
+
+ // Fallback: resolve remaining xrefs. Unqualified targets resolve
+ // relative to the current page; module-qualified targets (one ':')
+ // resolve against the component root derived from the property pages
+ // URL. Component-qualified targets can't be resolved client-side and
+ // render as their display text.
+ var withXrefs = withRefs.replace(
+ /xref:([^[\]]+?)\.adoc(?:#([^[\]]*))?\[([^\]]*)\]/g,
function (match, path, anchor, display) {
- var href = path.replace(/^\.\//, '') + '/'
+ var label = display || path.split('/').pop()
+ var href
+ var parts = path.split(':')
+ if (parts.length === 1) {
+ href = path.replace(/^\.\//, '')
+ } else if (parts.length === 2) {
+ var pagesUrl = getPropertiesPagesUrl()
+ if (!pagesUrl) return label
+ var componentRoot = pagesUrl.replace(/reference\/properties\/cluster-properties\/?$/, '')
+ href = componentRoot + parts[0] + '/' + parts[1].replace(/^\.\//, '')
+ } else {
+ return label
+ }
+ // Antora indexifies page URLs: index pages drop the final segment.
+ href = href.replace(/\/index$/, '') + '/'
if (anchor) href += '#' + anchor
- return '' + display + ''
+ return '' + label + ''
}
)
// Restore preserved links
- var result = withXrefs.replace(/___LINK_(\d+)___/g, function (match, index) {
+ return withXrefs.replace(/___LINK_(\d+)___/g, function (match, index) {
return linkPlaceholders[parseInt(index, 10)] || match
})
+ }
+
+ /**
+ * Format description - evaluate conditionals, then render paragraphs and
+ * bullet lists so multi-line descriptions don't collapse into one blob.
+ * With summaryOnly, keep just the first block (the summary sentence) and
+ * mark the truncation with an ellipsis.
+ */
+ function formatDescription (text, summaryOnly) {
+ if (!text) return ''
+
+ var cleaned = stripConditionals(String(text), definedConditionalAttributes())
+ var blocks = []
+ var paragraph = []
+ var list = null
+
+ function flushParagraph () {
+ if (paragraph.length) {
+ blocks.push('' + formatInline(paragraph.join(' ')) + '
')
+ paragraph = []
+ }
+ }
+ function flushList () {
+ if (list) {
+ blocks.push('' + list.map(function (item) { return '- ' + formatInline(item) + '
' }).join('') + '
')
+ list = null
+ }
+ }
+
+ cleaned.split('\n').forEach(function (rawLine) {
+ var line = rawLine.trim()
+ if (!line) {
+ flushParagraph()
+ flushList()
+ return
+ }
+ var item = line.match(/^[*-]\s+(.*)$/)
+ if (item) {
+ flushParagraph()
+ if (!list) list = []
+ list.push(item[1])
+ return
+ }
+ if (list) {
+ // Continuation of the previous list item
+ list[list.length - 1] += ' ' + line
+ return
+ }
+ paragraph.push(line)
+ })
+ flushParagraph()
+ flushList()
- return result
+ if (summaryOnly && blocks.length > 1) {
+ var first = blocks[0]
+ if (first.slice(-4) === '') {
+ return first.slice(0, -4) + '…'
+ }
+ return first + '…
'
+ }
+ return blocks.join('')
}
/**
@@ -363,16 +575,31 @@
return
}
- // Create a Set for fast lookup
- var propertyNames = new Set(Object.keys(properties))
-
- // Scope: opt-in pages look at all elements in the article
+ // Scope: only elements marked by the prop: macro are decorated
var article = document.querySelector('article.doc')
if (!article) return
- var codeElements = article.querySelectorAll('code:not(.has-property-tooltip)')
+ var codeElements = article.querySelectorAll(
+ 'code[data-property-name]:not(.has-property-tooltip), code.property-ref:not(.has-property-tooltip)'
+ )
var isTouch = isTouchDevice()
+ // Only the first mention of a property in a paragraph (or list item,
+ // table cell, ...) gets a tooltip. Repeats render as plain code so a
+ // dense paragraph isn't wall-to-wall dotted underlines.
+ var decoratedContainers = new WeakMap()
+ var isRepeatMention = function (codeEl, name) {
+ var container = codeEl.closest('p, li, td, th, dt, dd') || codeEl.parentElement || article
+ var seen = decoratedContainers.get(container)
+ if (seen && seen.has(name)) return true
+ if (!seen) {
+ seen = new Set()
+ decoratedContainers.set(container, seen)
+ }
+ seen.add(name)
+ return false
+ }
+
var getTippyConfig = function (content) {
return {
content: content,
@@ -384,14 +611,24 @@
appendTo: document.body,
trigger: isTouch ? 'click' : 'mouseenter focus',
hideOnClick: isTouch ? 'toggle' : true,
+ // Same show delay as the glossary and enterprise tooltips, so
+ // dragging the cursor across a paragraph doesn't fire previews.
+ delay: [200, 0],
+ popperOptions: {
+ modifiers: [
+ { name: 'preventOverflow', options: { boundary: 'viewport' } },
+ { name: 'flip', options: { fallbackPlacements: ['bottom', 'top'] } },
+ ],
+ },
}
}
codeElements.forEach(function (codeEl) {
- var text = codeEl.textContent.trim()
+ var text = codeEl.getAttribute('data-property-name') || codeEl.textContent.trim()
- // Check if this code element matches a property name
- if (propertyNames.has(text)) {
+ // Look up the marked property in the published data
+ if (Object.prototype.hasOwnProperty.call(properties, text)) {
+ if (isRepeatMention(codeEl, text)) return
var prop = properties[text]
var tooltipContent = createPropertyTooltip(prop)
diff --git a/src/partials/head-meta.hbs b/src/partials/head-meta.hbs
index 3193ee8d..b9e664a7 100644
--- a/src/partials/head-meta.hbs
+++ b/src/partials/head-meta.hbs
@@ -42,6 +42,24 @@
{{/if}}
{{/with}}
{{/if}}
+{{!--
+ Component-local property pages base for tooltip doc links. Resolved in the
+ current page's own component (resolve-resource defaults to the page's
+ component and version), so cloud pages link to cloud's property pages and
+ streaming pages to streaming's. Omitted when the component has no property
+ pages; the JS then falls back to streaming's URL space.
+--}}
+{{#with (resolve-resource 'reference:properties/cluster-properties.adoc' fallback='')}}
+
+{{/with}}
+{{!--
+ Cloud components set page-cloud: true in antora.yml. Property tooltip
+ descriptions carry ifdef::env-cloud conditionals; this flag tells the
+ tooltip formatter which branch to render.
+--}}
+{{#if page.attributes.cloud}}
+
+{{/if}}
{{!--
Connect JSON URL for Bloblang tooltips
diff --git a/src/static/redpanda-properties.json b/src/static/redpanda-properties.json
index 71dba70f..79b2f5a5 100644
--- a/src/static/redpanda-properties.json
+++ b/src/static/redpanda-properties.json
@@ -68,7 +68,7 @@
"name": "group_max_session_timeout_ms",
"type": "integer",
"default": 300000,
- "description": "The maximum session timeout for consumer groups. If a consumer does not send a heartbeat within this time, it is considered dead and removed from the group.",
+ "description": "The maximum session timeout for consumer groups. If a consumer does not send a heartbeat within this time, it is considered dead and removed from the group. Works together with prop:log_segment_size[link=true] and the legacy config_ref:enable_sasl,true,cluster-properties[`enable_sasl`] form renders too.",
"config_scope": "cluster",
"needs_restart": false,
"cloud_supported": true,
@@ -81,7 +81,7 @@
"name": "redpanda.storage.mode",
"type": "string",
"default": "tiered",
- "description": "Controls how data is stored for topics. Valid values are `local` (data stored only on local disk) and `tiered` (data can be offloaded to object storage).",
+ "description": "The storage mode for a topic. Determines how topic data is stored and whether it is eligible for upload to object storage.\n\nAccepted values:\n\n* `local`: Topic data is stored only on the broker's local disk. Object storage upload is disabled for the topic, regardless of cluster-level Tiered Storage settings.\nifndef::env-cloud[]\n* `tiered`: Topic data is stored on local disk and also uploaded to object storage. Enables xref:manage:tiered-storage/tiered-storage.adoc[Tiered Storage] for the topic.\n* `cloud`: Topic data is stored in object storage using the xref:develop:manage-topics/cloud-topics/index.adoc[Cloud Topics] architecture. Local storage is used only as a write buffer.\nendif::[]\nifdef::env-cloud[]\n* `tiered`: Topic data is stored on local disk and also uploaded to object storage. Enables Tiered Storage for the topic.\n* `cloud`: Topic data is stored in object storage using the Cloud Topics architecture. Local storage is used only as a write buffer.\nendif::[]\n* `unset`: Specifies that the topic's storage mode is unset, regardless of the cluster default. The topic may still have Tiered Storage enabled through the legacy properties `redpanda.remote.read` and `redpanda.remote.write`.\n\nStarting in Redpanda v26.2, the `tiered` value is an alias that resolves to Tiered Storage v1 or v2 when the topic is created, according to the `default_redpanda_storage_mode_tiered_impl` cluster property. The resolved version is reported by the read-only <> property, and the storage mode of a topic displays as `tiered` for both versions.\n\nThis property overrides the cluster-wide `default_redpanda_storage_mode` setting for individual topics.",
"config_scope": "topic",
"needs_restart": false,
"cloud_supported": true,
@@ -100,6 +100,18 @@
"is_deprecated": false,
"is_enterprise": false,
"visibility": "user"
+ },
+ "redpanda.storage.mode.impl": {
+ "name": "redpanda.storage.mode.impl",
+ "type": "string",
+ "default": null,
+ "description": "Read-only property reporting the resolved Tiered Storage implementation version (`tiered_v1` or `tiered_v2`) for a topic whose <> is `tiered`.",
+ "config_scope": "topic",
+ "needs_restart": false,
+ "cloud_supported": true,
+ "is_deprecated": false,
+ "is_enterprise": false,
+ "visibility": "user"
}
}
-}
+}
\ No newline at end of file