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('
' + badges.join(' ') + '
') } - // Description + // Description: first paragraph only. The tooltip is a preview; the full + // accepted-values detail lives at the "View full documentation" anchor. if (prop.description) { - parts.push('
' + formatDescription(prop.description) + '
') + parts.push('
' + formatDescription(prop.description, true) + '
') } // Default value @@ -276,15 +296,8 @@ parts.push('
Range: ' + range.join(', ') + '
') } - // Link to full documentation (use current page version) - var scope = prop.configScope || 'cluster' - var version = getDocVersion() - // AsciiDoc auto-ID generation: dots are removed, underscores become hyphens - // e.g., "redpanda.storage.mode" -> "redpandastoragemode" - // e.g., "log_retention_ms" -> "log-retention-ms" - var anchor = prop.name.replace(/\./g, '').replace(/_/g, '-') - var docUrl = '/' + version + '/reference/properties/' + scope + '-properties/#' + anchor - parts.push('View full documentation →') + // Link to full documentation, relative to the current page's component + parts.push('View full documentation →') return '
' + parts.join('') + '
' } @@ -300,14 +313,111 @@ } /** - * Format description - sanitize HTML while preserving safe links and code + * Build the component-relative documentation URL for a property. + * AsciiDoc auto-ID generation: dots are removed, underscores become hyphens + * e.g., "redpanda.storage.mode" -> "redpandastoragemode" + * e.g., "log_retention_ms" -> "log-retention-ms" + */ + function propertyAnchor (name) { + return name.replace(/\./g, '').replace(/_/g, '-') + } + + function buildDocUrl (prop) { + var scope = prop.configScope || 'cluster' + var anchor = propertyAnchor(prop.name) + var pagesUrl = getPropertiesPagesUrl() + if (pagesUrl) { + // Swap the scope into the component-resolved cluster-properties URL. + return pagesUrl.replace('cluster-properties', scope + '-properties') + '#' + anchor + } + // Fallback for pages without the meta tag: streaming URL space. + return '/' + getDocVersion() + '/reference/properties/' + scope + '-properties/#' + anchor + } + + /** + * Find a property whose generated heading anchor matches the given anchor, + * so <> internal references can link across property pages. + */ + var anchorIndex = null + function propertyForAnchor (anchor) { + if (!propertiesData) return null + if (!anchorIndex) { + anchorIndex = {} + Object.keys(propertiesData).forEach(function (name) { + anchorIndex[propertyAnchor(name)] = propertiesData[name] + }) + } + return anchorIndex[anchor] || null + } + + /** + * Attribute names considered "defined" when evaluating ifdef/ifndef + * conditionals in property descriptions. Property descriptions are shared + * between the self-managed and cloud sites, so pick the branch matching + * the site this page belongs to. + */ + function definedConditionalAttributes () { + var defined = [] + // Cloud components set page-cloud: true, which head-meta surfaces as a + // meta tag. Fall back to the component-resolved pages URL for pages + // built before the meta tag existed. + var meta = document.querySelector('meta[name="page-env-cloud"]') + var pagesUrl = getPropertiesPagesUrl() || '' + if ((meta && meta.content === 'true') || pagesUrl.indexOf('cloud-data-platform') !== -1) { + defined.push('env-cloud') + } + return defined + } + + /** + * Evaluate AsciiDoc preprocessor conditionals (ifdef/ifndef/endif) in a + * description, keeping only the lines for the current site. + */ + function stripConditionals (text, defined) { + var out = [] + var stack = [] + text.split('\n').forEach(function (line) { + var directive = line.match(/^\s*(ifdef|ifndef)::([^[\]]+)\[(.*)\]\s*$/) + if (directive) { + var attrs = directive[2] + var satisfied + if (attrs.indexOf(',') !== -1) { + satisfied = attrs.split(',').some(function (a) { return defined.indexOf(a.trim()) !== -1 }) + } else if (attrs.indexOf('+') !== -1) { + satisfied = attrs.split('+').every(function (a) { return defined.indexOf(a.trim()) !== -1 }) + } else { + satisfied = defined.indexOf(attrs.trim()) !== -1 + } + if (directive[1] === 'ifndef') satisfied = !satisfied + if (directive[3]) { + // Single-line form: ifdef::attr[content] + if (satisfied && stack.every(Boolean)) out.push(directive[3]) + } else { + stack.push(satisfied) + } + return + } + if (/^\s*endif::[^[\]]*\[\]\s*$/.test(line)) { + stack.pop() + return + } + if (stack.every(Boolean)) out.push(line) + }) + return out.join('\n') + } + + /** + * Format one run of inline text - sanitize HTML while preserving safe + * links, code spans, and property cross-references. * * Handles: * - Pre-resolved tags from JSON (safe, with href attribute) * - Backticks converted to 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 = 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