diff --git a/.htmltest.yml b/.htmltest.yml index 4e9cba8b44e8..3694384ababd 100644 --- a/.htmltest.yml +++ b/.htmltest.yml @@ -82,6 +82,9 @@ IgnoreURLs: # list of regexes of paths or URLs to be ignored - ^https://github\.com/open-telemetry/opentelemetry.io/tree/main/content/en/[^d] - ^https://github\.com/open-telemetry/opentelemetry.io/tree/main/content/en/docs/[^s] - ^https://github\.com/open-telemetry/opentelemetry.io/tree/main/content/en/docs/security + # TODO: remove once https://github.com/open-telemetry/opentelemetry.io/pull/9850 is merged; + # this file is new and won't exist on main until then: + - ^https://github\.com/open-telemetry/opentelemetry\.io/tree/main/content/en/docs/specs/otel-config/ # FIXME: same issue as for the OTel spec mentioned above: - ^https://github.com/open-telemetry/semantic-conventions/tree/main diff --git a/assets/js/configTypesAccordion.js b/assets/js/configTypesAccordion.js new file mode 100644 index 000000000000..0c21904c7e64 --- /dev/null +++ b/assets/js/configTypesAccordion.js @@ -0,0 +1,276 @@ +import { + expandAll, + collapseAll, + filterItems, + normalizeForSearch, + typeMatchesSearch, +} from './shared/accordionUtils.js'; + +const CONTAINER_SEL = '.config-types-accordion'; +const ACCORDION_ID = 'ct-accordion'; +const NO_RESULTS_ID = 'ct-no-results'; +const COUNT_ID = 'ct-count'; + +// ── i18n ────────────────────────────────────────────────────────────────────── + +function readI18n(container) { + const d = container.dataset; + return { + search: d.i18nSearch, + filterAll: d.i18nFilterAll, + filterStable: d.i18nFilterStable, + filterExperimental: d.i18nFilterExperimental, + expandAll: d.i18nExpandAll, + collapseAll: d.i18nCollapseAll, + noResults: d.i18nNoResults, + loading: d.i18nLoading, + colType: d.i18nColType, + colConstraints: d.i18nColConstraints, + colDescription: d.i18nColDescription, + }; +} + +// ── Rendering ───────────────────────────────────────────────────────────────── + +function escapeAttr(str) { + return String(str) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); +} + +function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>'); +} + +function renderControls(types, i18n) { + const stableCount = types.filter((t) => !t.isExperimental).length; + const expCount = types.filter((t) => t.isExperimental).length; + const total = types.length; + + return ` +
+
+
+ +
+
+
+ + + +
+
+
+ + +
+
+
+ Showing ${total} of ${total} types +
+
`; +} + +function renderPropertiesTable(type, i18n) { + if (type.hasNoProperties) { + return `

No configurable properties.

`; + } + + const hasConstraints = type.properties.some((p) => p.constraints); + + const rows = type.properties + .map( + (prop) => ` + + ${escapeHtml(prop.name)} + ${escapeHtml(prop.type)} + ${hasConstraints ? `${escapeHtml(prop.constraints)}` : ''} + + `, + ) + .join(''); + + return ` + + + + + + ${hasConstraints ? `` : ''} + + + + ${rows} +
Name${escapeHtml(i18n.colType)}${escapeHtml(i18n.colConstraints)}${escapeHtml(i18n.colDescription)}
`; +} + +function renderTypeItem(type, i18n) { + const propCount = type.hasNoProperties ? 0 : (type.properties?.length ?? 0); + const countText = propCount === 1 ? '1 property' : `${propCount} properties`; + const constraintsHtml = type.constraints + ? `

Type constraints: ${escapeHtml(type.constraints)}

` + : ''; + + return ` +
+

+ +

+
+
+ ${renderPropertiesTable(type, i18n)} + ${constraintsHtml} +
+
+
`; +} + +function renderAccordion(types, i18n) { + const items = types.map((t) => renderTypeItem(t, i18n)).join(''); + return ` +
+ ${items} +
+

${escapeHtml(i18n.noResults)}

`; +} + +// ── Description injection ───────────────────────────────────────────────────── + +// Descriptions are pre-rendered safe HTML (ul/ol/li/a only, per configSchemaTransform.mjs). +// They are injected via innerHTML after the DOM is built to avoid double-escaping. +function injectDescriptions(container, types) { + for (const type of types) { + for (const prop of type.properties ?? []) { + if (!prop.description) continue; + const cell = container.querySelector( + `[data-type-id="${CSS.escape(type.id)}"] [data-prop-desc="${CSS.escape(prop.name)}"]`, + ); + if (cell) cell.innerHTML = prop.description; + } + } +} + +// ── Filter / search ─────────────────────────────────────────────────────────── + +function applyFilters(container, types, state) { + const accordion = container.querySelector(`#${ACCORDION_ID}`); + const countEl = container.querySelector(`#${COUNT_ID}`); + const noResults = container.querySelector(`#${NO_RESULTS_ID}`); + + const q = normalizeForSearch(state.query); + let count = 0; + + filterItems(accordion, (item) => { + const isExp = item.dataset.isExperimental === 'true'; + const typeObj = types.find((t) => t.id === item.dataset.typeId); + const filterMatch = + state.filter === 'all' || + (state.filter === 'stable' && !isExp) || + (state.filter === 'experimental' && isExp); + const searchMatch = typeMatchesSearch(typeObj, q); + if (filterMatch && searchMatch) count++; + return filterMatch && searchMatch; + }); + + if (countEl) + countEl.textContent = `Showing ${count} of ${types.length} types`; + if (noResults) noResults.classList.toggle('d-none', count > 0); +} + +// ── Event wiring ────────────────────────────────────────────────────────────── + +function wireControls(container, types) { + const accordion = container.querySelector(`#${ACCORDION_ID}`); + const state = { query: '', filter: 'all' }; + let debounceTimer; + + // Search + const searchInput = container.querySelector('#ct-search'); + if (searchInput) { + searchInput.addEventListener('input', () => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + state.query = searchInput.value; + applyFilters(container, types, state); + }, 300); + }); + } + + // Filter buttons + container.querySelectorAll('[data-ct-filter]').forEach((btn) => { + btn.addEventListener('click', () => { + container + .querySelectorAll('[data-ct-filter]') + .forEach((b) => b.classList.remove('active')); + btn.classList.add('active'); + state.filter = btn.dataset.ctFilter; + applyFilters(container, types, state); + }); + }); + + // Expand / collapse all + const expandBtn = container.querySelector('#ct-expand-all'); + const collapseBtn = container.querySelector('#ct-collapse-all'); + if (expandBtn) + expandBtn.addEventListener('click', () => expandAll(accordion)); + if (collapseBtn) + collapseBtn.addEventListener('click', () => collapseAll(accordion)); +} + +// ── Init ────────────────────────────────────────────────────────────────────── + +async function init() { + const container = document.querySelector(CONTAINER_SEL); + if (!container) return; + + const schemaUrl = container.dataset.schemaUrl; + const i18n = readI18n(container); + + container.innerHTML = `

${escapeHtml(i18n.loading)}

`; + + try { + const res = await fetch(schemaUrl); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + const types = data.types; + + container.innerHTML = + renderControls(types, i18n) + renderAccordion(types, i18n); + injectDescriptions(container, types); + wireControls(container, types); + } catch (err) { + container.innerHTML = `
Failed to load configuration types.
`; + console.error('config-types-accordion:', err); + } +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} diff --git a/assets/js/shared/accordionUtils.js b/assets/js/shared/accordionUtils.js new file mode 100644 index 000000000000..12a74ce8e52d --- /dev/null +++ b/assets/js/shared/accordionUtils.js @@ -0,0 +1,62 @@ +/** + * Shared utilities for config documentation accordions. + * Used by configTypesAccordion.js and configLangStatusAccordion.js. + */ + +export function expandAll(container) { + container.querySelectorAll('.accordion-collapse').forEach((el) => { + el.classList.add('show'); + const btn = el + .closest('.accordion-item') + ?.querySelector('.accordion-button'); + if (btn) { + btn.classList.remove('collapsed'); + btn.setAttribute('aria-expanded', 'true'); + } + }); +} + +export function collapseAll(container) { + container.querySelectorAll('.accordion-collapse').forEach((el) => { + el.classList.remove('show'); + const btn = el + .closest('.accordion-item') + ?.querySelector('.accordion-button'); + if (btn) { + btn.classList.add('collapsed'); + btn.setAttribute('aria-expanded', 'false'); + } + }); +} + +/** + * Show or hide accordion items based on a predicate. + * @param {Element} container + * @param {function(Element): boolean} predicate + */ +export function filterItems(container, predicate) { + container.querySelectorAll('.accordion-item').forEach((item) => { + item.classList.toggle('d-none', !predicate(item)); + }); +} + +export function normalizeForSearch(str) { + return str.toLowerCase().replace(/\s+/g, ' ').trim(); +} + +/** + * Check if a type object matches a search query. + * Searches type name, property names, and property types. + * Descriptions are not searched because they contain pre-rendered HTML. + * @param {{ name: string, properties?: Array<{name: string, type: string}> }} typeObj + * @param {string} normalizedQuery - result of normalizeForSearch() + */ +export function typeMatchesSearch(typeObj, normalizedQuery) { + if (!normalizedQuery) return true; + if (normalizeForSearch(typeObj.name).includes(normalizedQuery)) return true; + for (const prop of typeObj.properties ?? []) { + if (normalizeForSearch(prop.name).includes(normalizedQuery)) return true; + if (normalizeForSearch(prop.type).includes(normalizedQuery)) return true; + } + return false; +} diff --git a/assets/scss/_config_accordion_shared.scss b/assets/scss/_config_accordion_shared.scss new file mode 100644 index 000000000000..22ea4667db66 --- /dev/null +++ b/assets/scss/_config_accordion_shared.scss @@ -0,0 +1,22 @@ +// Shared styles for config documentation accordions (config types, lang status). + +.config-types-controls { + background-color: var(--bs-tertiary-bg); + border: 1px solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + padding: 0.75rem 1rem; +} + +// Visual indicator for experimental types: left border accent. +.accordion-item[data-is-experimental='true'] { + .accordion-button { + border-left: 3px solid var(--bs-warning); + } +} + +// Keep the property count readable when the accordion button is in its open +// (expanded) state — the button background darkens and `text-body-secondary` +// would otherwise become hard to read. +.accordion-button:not(.collapsed) .ct-prop-count { + opacity: 0.7; +} diff --git a/assets/scss/_config_types_accordion.scss b/assets/scss/_config_types_accordion.scss new file mode 100644 index 000000000000..8cae6c834f0a --- /dev/null +++ b/assets/scss/_config_types_accordion.scss @@ -0,0 +1,33 @@ +// Config types accordion — types-specific styles. + +.config-types-accordion { + .ct-props-table { + font-size: 0.875rem; + + code { + // Prevent double-shrink from Bootstrap's inline code styling. + font-size: inherit; + // Allow long type names (e.g. ExperimentalComposableRuleBasedSampler) to wrap. + white-space: normal; + } + + // Normalize lists inside pre-rendered description HTML. + td:last-child { + ul, + ol { + margin-bottom: 0; + padding-left: 1.25rem; + } + } + } + + // Type-level constraints note at the bottom of each accordion body. + .ct-type-constraints { + font-size: 0.8125rem; + color: var(--bs-secondary-color); + border-top: 1px solid var(--bs-border-color-translucent); + padding-top: 0.5rem; + margin-top: 0.75rem; + margin-bottom: 0; + } +} diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index ba2fa1fdc2cd..5ca8d41c9ca6 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -18,6 +18,8 @@ @import 'java'; @import 'navbar'; @import 'training'; +@import 'config_accordion_shared'; +@import 'config_types_accordion'; .otel-docs-spec { .td-page-meta__edit { diff --git a/config/_default/module-template.yaml b/config/_default/module-template.yaml index 28aec4876f61..12fd86408557 100644 --- a/config/_default/module-template.yaml +++ b/config/_default/module-template.yaml @@ -15,6 +15,8 @@ mounts: # Specs, currently en only - source: tmp/otel/specification target: content/docs/specs/otel + - source: content/en/docs/specs/otel-config/types.md + target: content/docs/specs/otel/configuration/types.md - source: tmp/opamp target: content/docs/specs/opamp - source: tmp/otlp/docs/specification.md diff --git a/content/en/docs/specs/otel-config/types.md b/content/en/docs/specs/otel-config/types.md new file mode 100644 index 000000000000..2a9df098ee3c --- /dev/null +++ b/content/en/docs/specs/otel-config/types.md @@ -0,0 +1,27 @@ +--- +title: Configuration Types Reference +linkTitle: Configuration Types +description: >- + Searchable reference for all types defined in the OpenTelemetry declarative + configuration schema, including their properties and constraints. +weight: 10 +# This file lives in the opentelemetry.io repo but is mounted into the +# docs/specs/otel/ hierarchy, which cascades github_repo/github_subdir from +# the opentelemetry-specification submodule. Override those params so the +# Docsy "View page source" link points to the correct repo. +github_repo: https://github.com/open-telemetry/opentelemetry.io +github_subdir: '' +path_base_for_github_subdir: '' +--- + +The OpenTelemetry +[declarative configuration](/docs/specs/otel/configuration/data-model/) schema +defines configuration types that describe the structure of SDK components +configurable via a configuration file. Types prefixed with `Experimental` are +subject to breaking changes without notice. + +For the full data model and schema, see +[Data Model](/docs/specs/otel/configuration/data-model/). For SDK-specific +usage, see [Configuration SDK](/docs/specs/otel/configuration/sdk/). + +{{< config-types-accordion >}} diff --git a/i18n/en.yaml b/i18n/en.yaml index a1d1fb184a8e..982f7dce52c1 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -42,3 +42,17 @@ collector_component_stability_footnote: >- For details about component stability levels, see the [OpenTelemetry Collector component stability definitions](https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/component-stability.md). + +# Config types accordion +config_types_search_label: Search types and properties +config_types_filter_all: All +config_types_filter_stable: Stable +config_types_filter_experimental: Experimental +config_types_expand_all: Expand all +config_types_collapse_all: Collapse all +config_types_no_results: No types match your search. +config_types_loading: Loading configuration types… +config_types_col_name: Name +config_types_col_type: Type +config_types_col_constraints: Constraints +config_types_col_description: Description diff --git a/layouts/_shortcodes/config-types-accordion.html b/layouts/_shortcodes/config-types-accordion.html new file mode 100644 index 000000000000..176f59e4568f --- /dev/null +++ b/layouts/_shortcodes/config-types-accordion.html @@ -0,0 +1,18 @@ +{{- $schemaUrl := "/schemas/config-types.json" -}} + +
+
+ +{{ partial "script.html" (dict "src" "js/configTypesAccordion.js") -}}