From 2b5e8ec28c9c6353ed7821d9d3d07168e99118b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 11:21:06 +0000 Subject: [PATCH 01/11] Add region selector toggle to docs Adds a "Select your region" toggle (US/EU/AP) at the top of any documentation page that references platform.robusta.dev or api.robusta.dev. Selecting EU or AP rewrites those hosts in-place to platform..robusta.dev and api..robusta.dev (covers prose, code blocks, and anchor hrefs). Choice is persisted in localStorage so it carries across pages. The toggle is injected by region-selector.js and only appears on pages that actually contain matching URLs. --- docs/_static/custom.css | 60 ++++++++++++ docs/_static/region-selector.js | 159 ++++++++++++++++++++++++++++++++ docs/conf.py | 2 +- 3 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 docs/_static/region-selector.js diff --git a/docs/_static/custom.css b/docs/_static/custom.css index c6a8be02c..1382c3efc 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -352,3 +352,63 @@ h3 { .success-icon { color: #28a745; /* or any green color you prefer */ } + +/* Region selector */ +.robusta-region-selector { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.6rem; + margin: 0.75rem 0 1.25rem; + padding: 0.6rem 0.85rem; + border: 1px solid var(--md-default-fg-color--lightest, #e0e0e0); + border-left: 3px solid var(--md-primary-fg-color, #1976d2); + border-radius: 4px; + background-color: var(--md-code-bg-color, #f5f5f5); + font-size: 0.8rem; +} + +.robusta-region-selector__label { + font-weight: 600; + color: var(--md-default-fg-color, #333); +} + +.robusta-region-selector__options { + display: inline-flex; + gap: 0.25rem; +} + +.robusta-region-selector__btn { + appearance: none; + border: 1px solid var(--md-default-fg-color--lightest, #ccc); + background: var(--md-default-bg-color, #fff); + color: var(--md-default-fg-color, #333); + padding: 0.25rem 0.7rem; + border-radius: 3px; + cursor: pointer; + font: inherit; + font-weight: 500; + line-height: 1.2; + transition: background-color 0.15s, color 0.15s, border-color 0.15s; +} + +.robusta-region-selector__btn:hover { + border-color: var(--md-primary-fg-color, #1976d2); +} + +.robusta-region-selector__btn.is-active { + background: var(--md-primary-fg-color, #1976d2); + border-color: var(--md-primary-fg-color, #1976d2); + color: #fff; +} + +.robusta-region-selector__note { + color: var(--md-default-fg-color--light, #666); + font-size: 0.75rem; +} + +@media (max-width: 600px) { + .robusta-region-selector__note { + flex-basis: 100%; + } +} diff --git a/docs/_static/region-selector.js b/docs/_static/region-selector.js new file mode 100644 index 000000000..1a1921637 --- /dev/null +++ b/docs/_static/region-selector.js @@ -0,0 +1,159 @@ +(function () { + "use strict"; + + const REGIONS = { + us: { label: "US", infix: "" }, + eu: { label: "EU", infix: ".eu" }, + ap: { label: "AP", infix: ".ap" }, + }; + const STORAGE_KEY = "robusta-docs-region"; + const URL_PATTERN = /\b(platform|api)(?:\.(?:eu|ap))?\.robusta\.dev\b/g; + const DETECT_PATTERN = /\b(?:platform|api)(?:\.(?:eu|ap))?\.robusta\.dev\b/; + + function getRegion() { + try { + const r = localStorage.getItem(STORAGE_KEY); + return REGIONS[r] ? r : "us"; + } catch (e) { + return "us"; + } + } + + function saveRegion(r) { + try { + localStorage.setItem(STORAGE_KEY, r); + } catch (e) {} + } + + function rewrite(text, regionKey) { + const infix = REGIONS[regionKey].infix; + return text.replace(URL_PATTERN, function (_, sub) { + return sub + infix + ".robusta.dev"; + }); + } + + const targets = []; + + function collectTextNodes(root) { + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { + acceptNode: function (node) { + if (!node.nodeValue || !DETECT_PATTERN.test(node.nodeValue)) { + return NodeFilter.FILTER_REJECT; + } + const parent = node.parentNode; + if (parent && (parent.tagName === "SCRIPT" || parent.tagName === "STYLE")) { + return NodeFilter.FILTER_REJECT; + } + return NodeFilter.FILTER_ACCEPT; + }, + }); + let node; + while ((node = walker.nextNode())) { + targets.push({ kind: "text", node: node, original: node.nodeValue }); + } + } + + function collectAttributes(root) { + const ATTR_NAMES = ["href", "src", "data-clipboard-text", "value", "title"]; + const selector = ATTR_NAMES.map(function (a) { return "[" + a + "]"; }).join(","); + root.querySelectorAll(selector).forEach(function (el) { + ATTR_NAMES.forEach(function (attr) { + const v = el.getAttribute(attr); + if (v && DETECT_PATTERN.test(v)) { + targets.push({ kind: "attr", node: el, attr: attr, original: v }); + } + }); + }); + } + + function applyRegion(regionKey) { + for (let i = 0; i < targets.length; i++) { + const t = targets[i]; + const next = rewrite(t.original, regionKey); + if (t.kind === "text") { + if (t.node.nodeValue !== next) t.node.nodeValue = next; + } else { + if (t.node.getAttribute(t.attr) !== next) t.node.setAttribute(t.attr, next); + } + } + } + + function buildToggle(current) { + const wrap = document.createElement("div"); + wrap.className = "robusta-region-selector"; + wrap.setAttribute("role", "region"); + wrap.setAttribute("aria-label", "Robusta region selector"); + + const label = document.createElement("span"); + label.className = "robusta-region-selector__label"; + label.textContent = "Select your region:"; + wrap.appendChild(label); + + const group = document.createElement("div"); + group.className = "robusta-region-selector__options"; + group.setAttribute("role", "radiogroup"); + + Object.keys(REGIONS).forEach(function (key) { + const btn = document.createElement("button"); + btn.type = "button"; + btn.setAttribute("role", "radio"); + btn.setAttribute("data-region", key); + btn.className = "robusta-region-selector__btn"; + btn.textContent = REGIONS[key].label; + const active = key === current; + btn.setAttribute("aria-checked", String(active)); + if (active) btn.classList.add("is-active"); + btn.addEventListener("click", function () { + const r = btn.getAttribute("data-region"); + saveRegion(r); + applyRegion(r); + group.querySelectorAll("button[data-region]").forEach(function (b) { + const isActive = b.getAttribute("data-region") === r; + b.classList.toggle("is-active", isActive); + b.setAttribute("aria-checked", String(isActive)); + }); + }); + group.appendChild(btn); + }); + + wrap.appendChild(group); + + const note = document.createElement("span"); + note.className = "robusta-region-selector__note"; + note.textContent = "URLs on this page will update to match."; + wrap.appendChild(note); + + return wrap; + } + + function init() { + const content = + document.querySelector(".md-content__inner") || + document.querySelector("article.md-content__inner") || + document.querySelector("article") || + document.querySelector("main"); + if (!content) return; + + collectTextNodes(content); + collectAttributes(content); + if (targets.length === 0) return; + + const region = getRegion(); + const toggle = buildToggle(region); + + const firstHeading = content.querySelector("h1"); + if (firstHeading && firstHeading.parentNode) { + firstHeading.parentNode.insertBefore(toggle, firstHeading.nextSibling); + } else { + content.insertBefore(toggle, content.firstChild); + } + + if (region !== "us") applyRegion(region); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/docs/conf.py b/docs/conf.py index 4ee9e6978..3ca866e42 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -370,7 +370,7 @@ "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css", ] -html_js_files = ["analytics.js"] +html_js_files = ["analytics.js", "region-selector.js"] html_favicon = "_static/favicon.png" From a2d785b05049bc3510f909fb232c666ec5fcdcbe Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 11:36:05 +0000 Subject: [PATCH 02/11] Fix region selector button styles under Material theme The Material theme's .md-typeset button reset was more specific than the class-only selectors, so the buttons rendered as bare text. Scope the rules under .md-typeset and pin the visual properties with !important so the toggle renders as a proper segmented control in both light and dark modes. Persistence across pages was already handled via localStorage; no JS changes needed. --- docs/_static/custom.css | 66 ++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 1382c3efc..687268709 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -354,61 +354,85 @@ h3 { } /* Region selector */ +.md-typeset .robusta-region-selector, .robusta-region-selector { - display: flex; + display: flex !important; flex-wrap: wrap; align-items: center; - gap: 0.6rem; - margin: 0.75rem 0 1.25rem; - padding: 0.6rem 0.85rem; - border: 1px solid var(--md-default-fg-color--lightest, #e0e0e0); + gap: 0.75rem; + margin: 1rem 0 1.5rem !important; + padding: 0.7rem 0.9rem; + border: 1px solid var(--md-default-fg-color--lightest, rgba(0, 0, 0, 0.12)); border-left: 3px solid var(--md-primary-fg-color, #1976d2); border-radius: 4px; - background-color: var(--md-code-bg-color, #f5f5f5); + background-color: var(--md-code-bg-color, rgba(0, 0, 0, 0.04)); font-size: 0.8rem; + line-height: 1.4; } +.md-typeset .robusta-region-selector__label, .robusta-region-selector__label { font-weight: 600; - color: var(--md-default-fg-color, #333); + color: var(--md-default-fg-color, inherit); + margin-right: 0.25rem; } +.md-typeset .robusta-region-selector__options, .robusta-region-selector__options { - display: inline-flex; - gap: 0.25rem; + display: inline-flex !important; + gap: 0.35rem; + align-items: center; } +.md-typeset .robusta-region-selector__btn, .robusta-region-selector__btn { + display: inline-block !important; + -webkit-appearance: none; appearance: none; - border: 1px solid var(--md-default-fg-color--lightest, #ccc); - background: var(--md-default-bg-color, #fff); - color: var(--md-default-fg-color, #333); - padding: 0.25rem 0.7rem; - border-radius: 3px; + border: 1px solid var(--md-default-fg-color--lighter, rgba(0, 0, 0, 0.26)) !important; + background: var(--md-default-bg-color, #fff) !important; + color: var(--md-default-fg-color, #333) !important; + padding: 0.3rem 0.85rem !important; + border-radius: 3px !important; cursor: pointer; font: inherit; font-weight: 500; line-height: 1.2; - transition: background-color 0.15s, color 0.15s, border-color 0.15s; + min-width: 2.5rem; + text-align: center; + box-shadow: none; + transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease; } +.md-typeset .robusta-region-selector__btn:hover, .robusta-region-selector__btn:hover { - border-color: var(--md-primary-fg-color, #1976d2); + border-color: var(--md-primary-fg-color, #1976d2) !important; + color: var(--md-primary-fg-color, #1976d2) !important; } +.md-typeset .robusta-region-selector__btn.is-active, .robusta-region-selector__btn.is-active { - background: var(--md-primary-fg-color, #1976d2); - border-color: var(--md-primary-fg-color, #1976d2); - color: #fff; + background: var(--md-primary-fg-color, #1976d2) !important; + border-color: var(--md-primary-fg-color, #1976d2) !important; + color: var(--md-primary-bg-color, #fff) !important; +} + +.md-typeset .robusta-region-selector__btn:focus-visible, +.robusta-region-selector__btn:focus-visible { + outline: 2px solid var(--md-accent-fg-color, #1976d2); + outline-offset: 2px; } +.md-typeset .robusta-region-selector__note, .robusta-region-selector__note { - color: var(--md-default-fg-color--light, #666); - font-size: 0.75rem; + color: var(--md-default-fg-color--light, rgba(0, 0, 0, 0.6)); + font-size: 0.72rem; + margin-left: auto; } @media (max-width: 600px) { .robusta-region-selector__note { flex-basis: 100%; + margin-left: 0; } } From 1e066d41d6e7a323c47baa94bcb38bc252e61b3b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 11:44:10 +0000 Subject: [PATCH 03/11] Make region selector idempotent and instant-nav aware - init() now clears the targets array and removes any existing toggle before re-injecting, so it can be called repeatedly without duplicating UI or leaking stale node references. - Subscribe to the theme's document$ observable so the toggle is re-injected after Material/Sphinx-Immaterial instant navigations, which swap the content area without firing DOMContentLoaded. - Always call applyRegion on init (including "us") so any stored selection is consistently reapplied after navigation. --- docs/_static/region-selector.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/_static/region-selector.js b/docs/_static/region-selector.js index 1a1921637..8d72c8ca2 100644 --- a/docs/_static/region-selector.js +++ b/docs/_static/region-selector.js @@ -34,6 +34,10 @@ const targets = []; + function resetTargets() { + targets.length = 0; + } + function collectTextNodes(root) { const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { @@ -134,6 +138,13 @@ document.querySelector("main"); if (!content) return; + resetTargets(); + + const existing = content.querySelector(".robusta-region-selector"); + if (existing && existing.parentNode) { + existing.parentNode.removeChild(existing); + } + collectTextNodes(content); collectAttributes(content); if (targets.length === 0) return; @@ -148,7 +159,7 @@ content.insertBefore(toggle, content.firstChild); } - if (region !== "us") applyRegion(region); + applyRegion(region); } if (document.readyState === "loading") { @@ -156,4 +167,11 @@ } else { init(); } + + // Material/Sphinx-Immaterial instant navigation swaps the content area + // without firing DOMContentLoaded. The theme exposes an RxJS `document$` + // observable that emits on every swap; re-run init() on each emission. + if (typeof window !== "undefined" && window.document$ && typeof window.document$.subscribe === "function") { + window.document$.subscribe(init); + } })(); From 123a21694eae389eafe78b905324a061a820d143 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 11:53:14 +0000 Subject: [PATCH 04/11] Replace page-header region toggle with per-component selectors Introduces two RST directives for embedding region-aware Robusta URLs and code blocks inline in the docs: .. robusta-url:: https://api.robusta.dev/api/alerts .. robusta-code:: bash curl https://api.robusta.dev/api/alerts -H 'Authorization: ...' Each rendered component (".robusta-region-box") now carries its own US / EU / AP selector at the top of its frame. Clicking any selector on a page syncs every other component on the same page in lock-step and persists the choice to localStorage so it sticks across navigation. Implementation: - New Sphinx extension docs/_ext/region_box.py defines RobustaUrlDirective and RobustaCodeDirective and is registered in conf.py. - region-selector.js no longer injects a page-header toggle. It scans for .robusta-region-box elements, injects an inline selector bar into each, collects URL targets from the box body, and exposes a single syncAll() that re-applies the chosen region across every box. - custom.css restyles the toggle as a tight segmented control sitting flush atop the URL / code frame. - send-alerts-api.rst is migrated to use both directives as a worked example. Remaining docs continue to render their original URLs as-is until they are migrated to the new directives. --- docs/_ext/region_box.py | 85 ++++++++++++ docs/_static/custom.css | 129 ++++++++++++------ docs/_static/region-selector.js | 127 +++++++++-------- docs/conf.py | 1 + .../exporting/send-alerts-api.rst | 8 +- 5 files changed, 245 insertions(+), 105 deletions(-) create mode 100644 docs/_ext/region_box.py diff --git a/docs/_ext/region_box.py b/docs/_ext/region_box.py new file mode 100644 index 000000000..22ab1e854 --- /dev/null +++ b/docs/_ext/region_box.py @@ -0,0 +1,85 @@ +""" +Directives for Robusta platform URL / code blocks with an embedded region selector. + +Usage in .rst:: + + .. robusta-url:: https://platform.robusta.dev/account + + .. robusta-code:: bash + + curl --location --request POST 'https://api.robusta.dev/api/alerts' \ + --header 'Authorization: Bearer API-KEY' + +The rendered HTML carries the ``robusta-region-box`` class; the +client-side script in ``_static/region-selector.js`` injects the +US/EU/AP selector and keeps every box on the page in sync. +""" + +from html import escape + +from docutils import nodes +from sphinx.util.docutils import SphinxDirective + + +class RobustaUrlDirective(SphinxDirective): + has_content = True + required_arguments = 0 + optional_arguments = 1 + final_argument_whitespace = True + + def run(self): + if self.arguments: + url = self.arguments[0].strip() + else: + url = "\n".join(self.content).strip() + if not url: + return [ + self.state.document.reporter.warning( + "robusta-url requires a URL argument or content", + line=self.lineno, + ) + ] + + url_attr = escape(url, quote=True) + url_text = escape(url) + html = ( + f'
' + f'
' + f'{url_text}' + f'
' + ) + return [nodes.raw("", html, format="html")] + + +class RobustaCodeDirective(SphinxDirective): + has_content = True + required_arguments = 0 + optional_arguments = 1 + final_argument_whitespace = False + + def run(self): + language = self.arguments[0] if self.arguments else "text" + code = "\n".join(self.content) + if not code.strip(): + return [ + self.state.document.reporter.warning( + "robusta-code requires a code block as content", + line=self.lineno, + ) + ] + + container = nodes.container(classes=["robusta-region-box", "robusta-region-box--code"]) + literal = nodes.literal_block(code, code) + literal["language"] = language + container += literal + return [container] + + +def setup(app): + app.add_directive("robusta-url", RobustaUrlDirective) + app.add_directive("robusta-code", RobustaCodeDirective) + return { + "version": "0.1", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 687268709..9c1b1bb06 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -353,86 +353,131 @@ h3 { color: #28a745; /* or any green color you prefer */ } -/* Region selector */ -.md-typeset .robusta-region-selector, -.robusta-region-selector { - display: flex !important; - flex-wrap: wrap; - align-items: center; - gap: 0.75rem; +/* Robusta region-aware URL / code component */ +.md-typeset .robusta-region-box, +.robusta-region-box { margin: 1rem 0 1.5rem !important; - padding: 0.7rem 0.9rem; border: 1px solid var(--md-default-fg-color--lightest, rgba(0, 0, 0, 0.12)); - border-left: 3px solid var(--md-primary-fg-color, #1976d2); border-radius: 4px; background-color: var(--md-code-bg-color, rgba(0, 0, 0, 0.04)); - font-size: 0.8rem; - line-height: 1.4; + overflow: hidden; +} + +.md-typeset .robusta-region-box__bar, +.robusta-region-box__bar { + display: flex !important; + align-items: center; + gap: 0.6rem; + padding: 0.35rem 0.6rem; + background-color: var(--md-default-fg-color--lightest, rgba(0, 0, 0, 0.06)); + border-bottom: 1px solid var(--md-default-fg-color--lightest, rgba(0, 0, 0, 0.08)); + font-size: 0.72rem; + line-height: 1.2; } -.md-typeset .robusta-region-selector__label, -.robusta-region-selector__label { +.md-typeset .robusta-region-box__bar-label, +.robusta-region-box__bar-label { font-weight: 600; - color: var(--md-default-fg-color, inherit); - margin-right: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--md-default-fg-color--light, rgba(0, 0, 0, 0.6)); } -.md-typeset .robusta-region-selector__options, -.robusta-region-selector__options { +.md-typeset .robusta-region-box__region-options, +.robusta-region-box__region-options { display: inline-flex !important; - gap: 0.35rem; align-items: center; + gap: 0; + border-radius: 3px; + overflow: hidden; } -.md-typeset .robusta-region-selector__btn, -.robusta-region-selector__btn { - display: inline-block !important; +.md-typeset .robusta-region-box__region-btn, +.robusta-region-box__region-btn { -webkit-appearance: none; appearance: none; + display: inline-block !important; border: 1px solid var(--md-default-fg-color--lighter, rgba(0, 0, 0, 0.26)) !important; background: var(--md-default-bg-color, #fff) !important; color: var(--md-default-fg-color, #333) !important; - padding: 0.3rem 0.85rem !important; - border-radius: 3px !important; + padding: 0.2rem 0.7rem !important; + margin: 0 !important; cursor: pointer; font: inherit; - font-weight: 500; + font-size: 0.72rem !important; + font-weight: 600; line-height: 1.2; - min-width: 2.5rem; + min-width: 2.2rem; text-align: center; box-shadow: none; + border-radius: 0 !important; transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease; } -.md-typeset .robusta-region-selector__btn:hover, -.robusta-region-selector__btn:hover { - border-color: var(--md-primary-fg-color, #1976d2) !important; +.md-typeset .robusta-region-box__region-btn + .robusta-region-box__region-btn, +.robusta-region-box__region-btn + .robusta-region-box__region-btn { + border-left-width: 0 !important; +} + +.md-typeset .robusta-region-box__region-btn:first-child, +.robusta-region-box__region-btn:first-child { + border-top-left-radius: 3px !important; + border-bottom-left-radius: 3px !important; +} + +.md-typeset .robusta-region-box__region-btn:last-child, +.robusta-region-box__region-btn:last-child { + border-top-right-radius: 3px !important; + border-bottom-right-radius: 3px !important; +} + +.md-typeset .robusta-region-box__region-btn:hover, +.robusta-region-box__region-btn:hover { color: var(--md-primary-fg-color, #1976d2) !important; } -.md-typeset .robusta-region-selector__btn.is-active, -.robusta-region-selector__btn.is-active { +.md-typeset .robusta-region-box__region-btn.is-active, +.robusta-region-box__region-btn.is-active { background: var(--md-primary-fg-color, #1976d2) !important; border-color: var(--md-primary-fg-color, #1976d2) !important; color: var(--md-primary-bg-color, #fff) !important; + z-index: 1; + position: relative; } -.md-typeset .robusta-region-selector__btn:focus-visible, -.robusta-region-selector__btn:focus-visible { +.md-typeset .robusta-region-box__region-btn:focus-visible, +.robusta-region-box__region-btn:focus-visible { outline: 2px solid var(--md-accent-fg-color, #1976d2); outline-offset: 2px; + z-index: 1; + position: relative; } -.md-typeset .robusta-region-selector__note, -.robusta-region-selector__note { - color: var(--md-default-fg-color--light, rgba(0, 0, 0, 0.6)); - font-size: 0.72rem; - margin-left: auto; +/* URL body */ +.md-typeset .robusta-region-box__body, +.robusta-region-box__body { + padding: 0.6rem 0.85rem; + font-family: var(--md-code-font, "Roboto Mono", monospace); + font-size: 0.78rem; + word-break: break-all; +} + +.md-typeset .robusta-region-box--url a.robusta-region-box__url, +.robusta-region-box--url a.robusta-region-box__url { + color: var(--md-typeset-a-color, var(--md-primary-fg-color, #1976d2)); + text-decoration: none; +} + +.md-typeset .robusta-region-box--url a.robusta-region-box__url:hover, +.robusta-region-box--url a.robusta-region-box__url:hover { + text-decoration: underline; } -@media (max-width: 600px) { - .robusta-region-selector__note { - flex-basis: 100%; - margin-left: 0; - } +/* Let the inner literal block sit flush inside the frame */ +.md-typeset .robusta-region-box--code > .highlight, +.md-typeset .robusta-region-box--code > div[class*="highlight"], +.md-typeset .robusta-region-box--code > pre { + margin: 0 !important; + border: 0 !important; + border-radius: 0 !important; } diff --git a/docs/_static/region-selector.js b/docs/_static/region-selector.js index 8d72c8ca2..bcdaeacb9 100644 --- a/docs/_static/region-selector.js +++ b/docs/_static/region-selector.js @@ -9,6 +9,8 @@ const STORAGE_KEY = "robusta-docs-region"; const URL_PATTERN = /\b(platform|api)(?:\.(?:eu|ap))?\.robusta\.dev\b/g; const DETECT_PATTERN = /\b(?:platform|api)(?:\.(?:eu|ap))?\.robusta\.dev\b/; + const BAR_CLASS = "robusta-region-box__bar"; + const BTN_CLASS = "robusta-region-box__region-btn"; function getRegion() { try { @@ -32,13 +34,12 @@ }); } - const targets = []; + // Each box on the page has its own collection of URL targets so its + // contents update in place when the region changes. + const boxes = []; - function resetTargets() { - targets.length = 0; - } - - function collectTextNodes(root) { + function collectTargets(root) { + const targets = []; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { if (!node.nodeValue || !DETECT_PATTERN.test(node.nodeValue)) { @@ -55,9 +56,6 @@ while ((node = walker.nextNode())) { targets.push({ kind: "text", node: node, original: node.nodeValue }); } - } - - function collectAttributes(root) { const ATTR_NAMES = ["href", "src", "data-clipboard-text", "value", "title"]; const selector = ATTR_NAMES.map(function (a) { return "[" + a + "]"; }).join(","); root.querySelectorAll(selector).forEach(function (el) { @@ -68,11 +66,12 @@ } }); }); + return targets; } - function applyRegion(regionKey) { - for (let i = 0; i < targets.length; i++) { - const t = targets[i]; + function applyToBox(box, regionKey) { + for (let i = 0; i < box.targets.length; i++) { + const t = box.targets[i]; const next = rewrite(t.original, regionKey); if (t.kind === "text") { if (t.node.nodeValue !== next) t.node.nodeValue = next; @@ -80,86 +79,94 @@ if (t.node.getAttribute(t.attr) !== next) t.node.setAttribute(t.attr, next); } } + updateBoxButtons(box, regionKey); } - function buildToggle(current) { - const wrap = document.createElement("div"); - wrap.className = "robusta-region-selector"; - wrap.setAttribute("role", "region"); - wrap.setAttribute("aria-label", "Robusta region selector"); + function updateBoxButtons(box, regionKey) { + box.buttons.forEach(function (btn) { + const isActive = btn.getAttribute("data-region") === regionKey; + btn.classList.toggle("is-active", isActive); + btn.setAttribute("aria-checked", String(isActive)); + }); + } + + function syncAll(regionKey) { + saveRegion(regionKey); + boxes.forEach(function (box) { + applyToBox(box, regionKey); + }); + } + + function buildBar(currentRegion) { + const bar = document.createElement("div"); + bar.className = BAR_CLASS; const label = document.createElement("span"); - label.className = "robusta-region-selector__label"; - label.textContent = "Select your region:"; - wrap.appendChild(label); + label.className = "robusta-region-box__bar-label"; + label.textContent = "Region"; + bar.appendChild(label); const group = document.createElement("div"); - group.className = "robusta-region-selector__options"; + group.className = "robusta-region-box__region-options"; group.setAttribute("role", "radiogroup"); + group.setAttribute("aria-label", "Select your Robusta region"); + const buttons = []; Object.keys(REGIONS).forEach(function (key) { const btn = document.createElement("button"); btn.type = "button"; btn.setAttribute("role", "radio"); btn.setAttribute("data-region", key); - btn.className = "robusta-region-selector__btn"; + btn.className = BTN_CLASS; btn.textContent = REGIONS[key].label; - const active = key === current; + const active = key === currentRegion; btn.setAttribute("aria-checked", String(active)); if (active) btn.classList.add("is-active"); btn.addEventListener("click", function () { - const r = btn.getAttribute("data-region"); - saveRegion(r); - applyRegion(r); - group.querySelectorAll("button[data-region]").forEach(function (b) { - const isActive = b.getAttribute("data-region") === r; - b.classList.toggle("is-active", isActive); - b.setAttribute("aria-checked", String(isActive)); - }); + syncAll(key); }); group.appendChild(btn); + buttons.push(btn); }); - wrap.appendChild(group); + bar.appendChild(group); + return { bar: bar, buttons: buttons }; + } - const note = document.createElement("span"); - note.className = "robusta-region-selector__note"; - note.textContent = "URLs on this page will update to match."; - wrap.appendChild(note); + function initBox(el, currentRegion) { + const existingBar = el.querySelector(":scope > ." + BAR_CLASS); + if (existingBar) existingBar.remove(); - return wrap; + const built = buildBar(currentRegion); + el.insertBefore(built.bar, el.firstChild); + + const targetRoot = el.querySelector(".robusta-region-box__body") || el; + const targets = collectTargets(targetRoot); + + const box = { + el: el, + bar: built.bar, + buttons: built.buttons, + targets: targets, + }; + boxes.push(box); + applyToBox(box, currentRegion); } function init() { + boxes.length = 0; const content = document.querySelector(".md-content__inner") || - document.querySelector("article.md-content__inner") || document.querySelector("article") || - document.querySelector("main"); + document.querySelector("main") || + document.body; if (!content) return; - resetTargets(); - - const existing = content.querySelector(".robusta-region-selector"); - if (existing && existing.parentNode) { - existing.parentNode.removeChild(existing); - } - - collectTextNodes(content); - collectAttributes(content); - if (targets.length === 0) return; - const region = getRegion(); - const toggle = buildToggle(region); - - const firstHeading = content.querySelector("h1"); - if (firstHeading && firstHeading.parentNode) { - firstHeading.parentNode.insertBefore(toggle, firstHeading.nextSibling); - } else { - content.insertBefore(toggle, content.firstChild); - } - - applyRegion(region); + const elements = content.querySelectorAll(".robusta-region-box"); + elements.forEach(function (el) { + initBox(el, region); + }); } if (document.readyState === "loading") { diff --git a/docs/conf.py b/docs/conf.py index 3ca866e42..a00a1619d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,6 +50,7 @@ "sphinx_design", "sphinxcontrib.images", "autorobusta", + "region_box", "sphinx_immaterial", "sphinxcontrib.details.directive", "sphinx_jinja", diff --git a/docs/configuration/exporting/send-alerts-api.rst b/docs/configuration/exporting/send-alerts-api.rst index 0fc423832..01e220662 100644 --- a/docs/configuration/exporting/send-alerts-api.rst +++ b/docs/configuration/exporting/send-alerts-api.rst @@ -21,8 +21,10 @@ Use this endpoint to send alert data to Robusta. You can send up to 1000 alerts .. _send-alerts-api: -POST https://api.robusta.dev/api/alerts -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +POST endpoint +^^^^^^^^^^^^^ + +.. robusta-url:: https://api.robusta.dev/api/alerts Request Body Schema """""""""""""""""""" @@ -133,7 +135,7 @@ Example Request Here is an example of a ``POST`` request to send a list of alerts: -.. code-block:: bash +.. robusta-code:: bash curl --location --request POST 'https://api.robusta.dev/api/alerts' \ --header 'Authorization: Bearer API-KEY' \ From 6f9f14d85b83ed99e8ecfe274297da07e0362cc2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 12:01:17 +0000 Subject: [PATCH 05/11] Rename selector label to 'Select Region' and migrate all docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes: 1. Rename the bar label from "Region" to "Select Region" in the selector UI. 2. Migrate every documentation page that mentions platform.robusta.dev or api.robusta.dev so the region selector is visible wherever the user encounters a Robusta endpoint. Concretely, replace .. code-block:: with .. robusta-code:: for any code block whose body contains a Robusta URL — 65 blocks across 36 files. Pages whose only mention is an inline external link (signup CTAs, prose references) are intentionally left as-is; the page-wide JS still rewrites those URLs to the selected region using the value persisted in localStorage from any page that does carry a selector. The JS also now collects URL targets from the whole content area rather than scoping them to each box, so a selector click on any component on the page rewrites every Robusta URL it can reach (prose, tables, code, anchor hrefs). --- docs/_static/region-selector.js | 76 ++++++++----------- .../coralogix_managed_prometheus.rst | 2 +- .../google-managed-alertmanager.rst | 4 +- .../grafana-self-hosted.rst | 4 +- .../alertmanager-integration/nagios.rst | 4 +- .../outofcluster-prometheus.rst | 4 +- .../pagerduty-alerting.rst | 10 +-- .../alertmanager-integration/solarwinds.rst | 2 +- .../exporting/alert-export-api.rst | 2 +- .../exporting/alert-statistics-api.rst | 2 +- .../exporting/configuration-changes-api.rst | 2 +- .../exporting/custom-webhooks.rst | 4 +- .../exporting/namespace-resources-api.rst | 2 +- .../exporting/prometheus-query-api.rst | 6 +- docs/configuration/exporting/rbac-api.rst | 16 ++-- .../exporting/send-events-api.rst | 4 +- .../exporting/send-events/alertmanager.rst | 4 +- .../exporting/send-events/aws-cloudwatch.rst | 4 +- .../exporting/send-events/azure-monitor.rst | 4 +- .../exporting/send-events/datadog.rst | 2 +- .../exporting/send-events/dynatrace.rst | 2 +- .../exporting/send-events/gcp-monitoring.rst | 4 +- .../exporting/send-events/grafana.rst | 2 +- .../exporting/send-events/nagios.rst | 4 +- .../exporting/send-events/newrelic.rst | 2 +- .../exporting/send-events/opsgenie.rst | 2 +- .../exporting/send-events/pagerduty.rst | 2 +- .../exporting/send-events/sentry.rst | 2 +- .../exporting/send-events/solarwinds.rst | 4 +- .../exporting/send-events/splunk.rst | 4 +- .../github-actions/holmes-pr-review.rst | 2 +- .../holmesgpt/holmes-chat-api.rst | 6 +- docs/configuration/resource-recommender.rst | 2 +- docs/configuration/sinks/slack.rst | 2 +- docs/configuration/sinks/webhook.rst | 2 +- .../triggers/elasticsearch.rst | 2 +- docs/setup-robusta/proxies.rst | 4 +- 37 files changed, 98 insertions(+), 108 deletions(-) diff --git a/docs/_static/region-selector.js b/docs/_static/region-selector.js index bcdaeacb9..b650de871 100644 --- a/docs/_static/region-selector.js +++ b/docs/_static/region-selector.js @@ -34,12 +34,13 @@ }); } - // Each box on the page has its own collection of URL targets so its - // contents update in place when the region changes. + // All URL occurrences across the page content (text nodes + relevant + // attributes). Boxes are separate — each holds its selector buttons so + // every box on the page stays visually in sync on region change. + const targets = []; const boxes = []; function collectTargets(root) { - const targets = []; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { if (!node.nodeValue || !DETECT_PATTERN.test(node.nodeValue)) { @@ -66,12 +67,11 @@ } }); }); - return targets; } - function applyToBox(box, regionKey) { - for (let i = 0; i < box.targets.length; i++) { - const t = box.targets[i]; + function applyRegion(regionKey) { + for (let i = 0; i < targets.length; i++) { + const t = targets[i]; const next = rewrite(t.original, regionKey); if (t.kind === "text") { if (t.node.nodeValue !== next) t.node.nodeValue = next; @@ -79,22 +79,20 @@ if (t.node.getAttribute(t.attr) !== next) t.node.setAttribute(t.attr, next); } } - updateBoxButtons(box, regionKey); - } - - function updateBoxButtons(box, regionKey) { - box.buttons.forEach(function (btn) { - const isActive = btn.getAttribute("data-region") === regionKey; - btn.classList.toggle("is-active", isActive); - btn.setAttribute("aria-checked", String(isActive)); - }); + for (let j = 0; j < boxes.length; j++) { + const buttons = boxes[j].buttons; + for (let k = 0; k < buttons.length; k++) { + const btn = buttons[k]; + const active = btn.getAttribute("data-region") === regionKey; + btn.classList.toggle("is-active", active); + btn.setAttribute("aria-checked", String(active)); + } + } } function syncAll(regionKey) { saveRegion(regionKey); - boxes.forEach(function (box) { - applyToBox(box, regionKey); - }); + applyRegion(regionKey); } function buildBar(currentRegion) { @@ -103,7 +101,7 @@ const label = document.createElement("span"); label.className = "robusta-region-box__bar-label"; - label.textContent = "Region"; + label.textContent = "Select Region"; bar.appendChild(label); const group = document.createElement("div"); @@ -133,28 +131,10 @@ return { bar: bar, buttons: buttons }; } - function initBox(el, currentRegion) { - const existingBar = el.querySelector(":scope > ." + BAR_CLASS); - if (existingBar) existingBar.remove(); - - const built = buildBar(currentRegion); - el.insertBefore(built.bar, el.firstChild); - - const targetRoot = el.querySelector(".robusta-region-box__body") || el; - const targets = collectTargets(targetRoot); - - const box = { - el: el, - bar: built.bar, - buttons: built.buttons, - targets: targets, - }; - boxes.push(box); - applyToBox(box, currentRegion); - } - function init() { + targets.length = 0; boxes.length = 0; + const content = document.querySelector(".md-content__inner") || document.querySelector("article") || @@ -163,10 +143,20 @@ if (!content) return; const region = getRegion(); - const elements = content.querySelectorAll(".robusta-region-box"); - elements.forEach(function (el) { - initBox(el, region); + + content.querySelectorAll(".robusta-region-box").forEach(function (el) { + const existingBar = el.querySelector(":scope > ." + BAR_CLASS); + if (existingBar) existingBar.remove(); + const built = buildBar(region); + el.insertBefore(built.bar, el.firstChild); + boxes.push({ buttons: built.buttons }); }); + + collectTargets(content); + + if (targets.length === 0 && boxes.length === 0) return; + + applyRegion(region); } if (document.readyState === "loading") { diff --git a/docs/configuration/alertmanager-integration/coralogix_managed_prometheus.rst b/docs/configuration/alertmanager-integration/coralogix_managed_prometheus.rst index 6ee721476..d86b016d6 100644 --- a/docs/configuration/alertmanager-integration/coralogix_managed_prometheus.rst +++ b/docs/configuration/alertmanager-integration/coralogix_managed_prometheus.rst @@ -20,7 +20,7 @@ Common Configuration (for both webhooks) 1. In the Coralogix site go to Data Flow, then Outbound Webhooks, and click ``Generic webhook``. 2. In the url insert: -.. code-block:: +.. robusta-code:: https://api.robusta.dev/integrations/generic/alertmanager diff --git a/docs/configuration/alertmanager-integration/google-managed-alertmanager.rst b/docs/configuration/alertmanager-integration/google-managed-alertmanager.rst index 18a5cdb1e..916a51d97 100644 --- a/docs/configuration/alertmanager-integration/google-managed-alertmanager.rst +++ b/docs/configuration/alertmanager-integration/google-managed-alertmanager.rst @@ -22,7 +22,7 @@ Configure the Alertmanager webhook Apply the following Secret in the GMP namespace (default ``gmp-public``). Replace ```` and ```` with your credentials. -.. code-block:: yaml +.. robusta-code:: yaml apiVersion: v1 kind: Secret @@ -103,7 +103,7 @@ Optional: verify credentials with curl You can manually validate the webhook and credentials by posting a sample alert: -.. code-block:: bash +.. robusta-code:: bash curl -X POST 'https://api.robusta.dev/integrations/generic/alertmanager' \ -H 'Authorization: Bearer ' \ diff --git a/docs/configuration/alertmanager-integration/grafana-self-hosted.rst b/docs/configuration/alertmanager-integration/grafana-self-hosted.rst index 0809277b0..8889bde88 100644 --- a/docs/configuration/alertmanager-integration/grafana-self-hosted.rst +++ b/docs/configuration/alertmanager-integration/grafana-self-hosted.rst @@ -44,7 +44,7 @@ Generate and save your new ``API Key`` Select ``Webhook`` from the Integration options. Add the following URL. Add your ``account_id`` to it: -.. code-block:: +.. robusta-code:: https://api.robusta.dev/integrations/alerts/grafana?account_id=YOUR_ACCOUNT_ID @@ -111,7 +111,7 @@ To configure it: 2. Insert the following URL: -.. code-block:: +.. robusta-code:: https://api.robusta.dev/integrations/generic/alertmanager diff --git a/docs/configuration/alertmanager-integration/nagios.rst b/docs/configuration/alertmanager-integration/nagios.rst index 437b527bf..541ddbd5f 100644 --- a/docs/configuration/alertmanager-integration/nagios.rst +++ b/docs/configuration/alertmanager-integration/nagios.rst @@ -14,7 +14,7 @@ Requirements - Robusta must already be deployed and running in your environment. - The Nagios host must be able to send `curl` requests to the Robusta API endpoint: - .. code-block:: + .. robusta-code:: https://api.robusta.dev/integrations/generic/nagios @@ -80,7 +80,7 @@ Step 4: Create the Bash Command Script Save this as `notify-robusta.sh`, ensure it's executable (`chmod +x notify-robusta.sh`), and Nagios can access it. -.. code-block:: bash +.. robusta-code:: bash #!/bin/sh diff --git a/docs/configuration/alertmanager-integration/outofcluster-prometheus.rst b/docs/configuration/alertmanager-integration/outofcluster-prometheus.rst index 10a857501..b3a87fc11 100644 --- a/docs/configuration/alertmanager-integration/outofcluster-prometheus.rst +++ b/docs/configuration/alertmanager-integration/outofcluster-prometheus.rst @@ -21,7 +21,7 @@ This integration lets your central Prometheus send alerts to Robusta, as if they .. admonition:: alertmanager.yaml - .. code-block:: yaml + .. robusta-code:: yaml receivers: - name: 'robusta' @@ -72,7 +72,7 @@ If you are using a third-party AlertManager and want to give it separate credent .. admonition:: alertmanager.yaml - .. code-block:: yaml + .. robusta-code:: yaml receivers: - name: 'robusta' diff --git a/docs/configuration/alertmanager-integration/pagerduty-alerting.rst b/docs/configuration/alertmanager-integration/pagerduty-alerting.rst index d765bb8a6..308620609 100644 --- a/docs/configuration/alertmanager-integration/pagerduty-alerting.rst +++ b/docs/configuration/alertmanager-integration/pagerduty-alerting.rst @@ -47,7 +47,7 @@ Set the following values: * **Webhook URL**: -.. code-block:: +.. robusta-code:: https://api.robusta.dev/integrations/generic/pagerduty/incidents @@ -88,7 +88,7 @@ Step 1: Create an Integration for Alertmanager 1. In Event Orchestration, create a new **Integration** named ``Alertmanager``. 2. Use the following webhook URL: -.. code-block:: +.. robusta-code:: https://api.robusta.dev/integrations/generic/pagerduty/alerts @@ -116,7 +116,7 @@ Step 3: Add a Webhook Action * **URL**: -.. code-block:: +.. robusta-code:: https://api.robusta.dev/integrations/generic/pagerduty/alerts @@ -171,7 +171,7 @@ Optional: Cluster Name via Query Param You can specify the target cluster using a query parameter in the webhook URL: -.. code-block:: +.. robusta-code:: https://api.robusta.dev/integrations/generic/pagerduty/incidents?cluster=your-cluster-name @@ -179,7 +179,7 @@ This is useful for multi-cluster setups where Robusta should assign findings to For example: -.. code-block:: +.. robusta-code:: https://api.robusta.dev/integrations/generic/pagerduty/incidents?cluster=test-cluster diff --git a/docs/configuration/alertmanager-integration/solarwinds.rst b/docs/configuration/alertmanager-integration/solarwinds.rst index b6d857b8a..6d9ff0cda 100644 --- a/docs/configuration/alertmanager-integration/solarwinds.rst +++ b/docs/configuration/alertmanager-integration/solarwinds.rst @@ -37,7 +37,7 @@ Step 3: Create a Webhook Configuration in SolarWinds - **Description**: Robusta Webhook - **Destination URL**: - .. code-block:: + .. robusta-code:: https://api.robusta.dev/integrations/generic/solarwinds?account_id=ACCOUNT_ID_HERE diff --git a/docs/configuration/exporting/alert-export-api.rst b/docs/configuration/exporting/alert-export-api.rst index dae4feab5..adc6a7f84 100644 --- a/docs/configuration/exporting/alert-export-api.rst +++ b/docs/configuration/exporting/alert-export-api.rst @@ -48,7 +48,7 @@ Example Request The following ``curl`` command demonstrates how to export alert history data for the ``CrashLoopBackoff`` alert: -.. code-block:: bash +.. robusta-code:: bash curl --location 'https://api.robusta.dev/api/query/alerts?alert_name=CrashLoopBackoff&account_id=ACCOUNT_ID&start_ts=2024-09-02T04%3A02%3A05.032Z&end_ts=2024-09-17T05%3A02%3A05.032Z' \ --header 'Authorization: Bearer API-KEY' diff --git a/docs/configuration/exporting/alert-statistics-api.rst b/docs/configuration/exporting/alert-statistics-api.rst index 9e79fd7b1..f969aa659 100644 --- a/docs/configuration/exporting/alert-statistics-api.rst +++ b/docs/configuration/exporting/alert-statistics-api.rst @@ -41,7 +41,7 @@ Example Request The following `curl` command demonstrates how to query aggregated alert data for a specified time range: -.. code-block:: bash +.. robusta-code:: bash curl --location 'https://api.robusta.dev/api/query/report?account_id=XXXXXX-XXXX_XXXX_XXXXX7&start_ts=2024-10-27T04:02:05.032Z&end_ts=2024-11-27T05:02:05.032Z' \ --header 'Authorization: Bearer API-KEY' diff --git a/docs/configuration/exporting/configuration-changes-api.rst b/docs/configuration/exporting/configuration-changes-api.rst index 3891990ea..0ef5b19b8 100644 --- a/docs/configuration/exporting/configuration-changes-api.rst +++ b/docs/configuration/exporting/configuration-changes-api.rst @@ -114,7 +114,7 @@ Example Request Here is an example of a ``POST`` request to send a list of configuration changes: -.. code-block:: bash +.. robusta-code:: bash curl --location --request POST 'https://api.robusta.dev/api/config-changes' \ --header 'Authorization: Bearer API-KEY' \ diff --git a/docs/configuration/exporting/custom-webhooks.rst b/docs/configuration/exporting/custom-webhooks.rst index a7b9bb629..2f8701dc6 100644 --- a/docs/configuration/exporting/custom-webhooks.rst +++ b/docs/configuration/exporting/custom-webhooks.rst @@ -18,7 +18,7 @@ Webhook Endpoint Send alerts to Robusta using the following endpoint: -.. code-block:: bash +.. robusta-code:: bash POST https://api.robusta.dev/api/alerts @@ -37,7 +37,7 @@ Quick Example Here's a simple example of sending a custom alert: -.. code-block:: bash +.. robusta-code:: bash curl --location --request POST 'https://api.robusta.dev/api/alerts' \ --header 'Authorization: Bearer YOUR_API_KEY' \ diff --git a/docs/configuration/exporting/namespace-resources-api.rst b/docs/configuration/exporting/namespace-resources-api.rst index 8e4141197..b604f04f0 100644 --- a/docs/configuration/exporting/namespace-resources-api.rst +++ b/docs/configuration/exporting/namespace-resources-api.rst @@ -63,7 +63,7 @@ Example Request Here is an example of a ``POST`` request to query the resource count in a namespace: -.. code-block:: bash +.. robusta-code:: bash curl --location 'https://api.robusta.dev/api/namespaces/resources' \ --header 'Authorization: Bearer API-KEY-HERE' \ diff --git a/docs/configuration/exporting/prometheus-query-api.rst b/docs/configuration/exporting/prometheus-query-api.rst index d02a94f38..245c44378 100644 --- a/docs/configuration/exporting/prometheus-query-api.rst +++ b/docs/configuration/exporting/prometheus-query-api.rst @@ -12,7 +12,7 @@ in your connected clusters. Endpoint -------- -.. code-block:: +.. robusta-code:: POST https://api.robusta.dev/api/accounts/{account_id}/clusters/{cluster_name}/prometheus/query @@ -86,7 +86,7 @@ Example Request Using duration (last 5 minutes): -.. code-block:: bash +.. robusta-code:: bash curl -X POST "https://api.robusta.dev/api/accounts/ACCOUNT_ID/clusters/CLUSTER_NAME/prometheus/query" \ -H "Authorization: Bearer YOUR_API_KEY" \ @@ -99,7 +99,7 @@ Using duration (last 5 minutes): Using date range: -.. code-block:: bash +.. robusta-code:: bash curl -X POST "https://api.robusta.dev/api/accounts/ACCOUNT_ID/clusters/CLUSTER_NAME/prometheus/query" \ -H "Authorization: Bearer YOUR_API_KEY" \ diff --git a/docs/configuration/exporting/rbac-api.rst b/docs/configuration/exporting/rbac-api.rst index 0f6f17697..b60133b21 100644 --- a/docs/configuration/exporting/rbac-api.rst +++ b/docs/configuration/exporting/rbac-api.rst @@ -56,7 +56,7 @@ Retrieve the current RBAC configuration for your account. **Request:** -.. code-block:: bash +.. robusta-code:: bash curl -X GET 'https://api.robusta.dev/api/rbac?account_id=YOUR_ACCOUNT_ID' \ -H 'Authorization: Bearer YOUR_API_KEY' @@ -109,7 +109,7 @@ Create or update the RBAC configuration for your account. **Request:** -.. code-block:: bash +.. robusta-code:: bash curl -X POST 'https://api.robusta.dev/api/rbac?account_id=YOUR_ACCOUNT_ID' \ -H 'Authorization: Bearer YOUR_API_KEY' \ @@ -186,7 +186,7 @@ Remove all RBAC configurations for your account. **Request:** -.. code-block:: bash +.. robusta-code:: bash curl -X DELETE 'https://api.robusta.dev/api/rbac?account_id=YOUR_ACCOUNT_ID' \ -H 'Authorization: Bearer YOUR_API_KEY' @@ -319,7 +319,7 @@ Examples **Set up namespace-level permissions for developers:** -.. code-block:: bash +.. robusta-code:: bash curl -X POST 'https://api.robusta.dev/api/rbac?account_id=YOUR_ACCOUNT_ID' \ -H 'Authorization: Bearer YOUR_API_KEY' \ @@ -349,7 +349,7 @@ Examples **Set up cluster-wide admin access:** -.. code-block:: bash +.. robusta-code:: bash curl -X POST 'https://api.robusta.dev/api/rbac?account_id=YOUR_ACCOUNT_ID' \ -H 'Authorization: Bearer YOUR_API_KEY' \ @@ -367,7 +367,7 @@ Examples **Complex configuration with multiple scopes and permission groups:** -.. code-block:: bash +.. robusta-code:: bash curl -X POST 'https://api.robusta.dev/api/rbac?account_id=YOUR_ACCOUNT_ID' \ -H 'Authorization: Bearer YOUR_API_KEY' \ @@ -429,14 +429,14 @@ Examples **Retrieve current configuration:** -.. code-block:: bash +.. robusta-code:: bash curl -X GET 'https://api.robusta.dev/api/rbac?account_id=YOUR_ACCOUNT_ID' \ -H 'Authorization: Bearer YOUR_API_KEY' **Clear all RBAC configurations:** -.. code-block:: bash +.. robusta-code:: bash curl -X DELETE 'https://api.robusta.dev/api/rbac?account_id=YOUR_ACCOUNT_ID' \ -H 'Authorization: Bearer YOUR_API_KEY' diff --git a/docs/configuration/exporting/send-events-api.rst b/docs/configuration/exporting/send-events-api.rst index 4fe9b94dc..fe940c8ec 100644 --- a/docs/configuration/exporting/send-events-api.rst +++ b/docs/configuration/exporting/send-events-api.rst @@ -27,7 +27,7 @@ This is the recommended ingestion path for new integrations. The legacy :doc:`Se Endpoint -------- -.. code-block:: +.. robusta-code:: POST https://api.robusta.dev/webhooks?type=alert&origin=&account_id= @@ -61,7 +61,7 @@ The key must be scoped to the ``account_id`` query parameter. Mismatches return Example Request --------------- -.. code-block:: bash +.. robusta-code:: bash curl --location --request POST \ 'https://api.robusta.dev/webhooks?type=alert&origin=datadog&account_id=ACCOUNT_ID' \ diff --git a/docs/configuration/exporting/send-events/alertmanager.rst b/docs/configuration/exporting/send-events/alertmanager.rst index e695a3dcf..9336e09ac 100644 --- a/docs/configuration/exporting/send-events/alertmanager.rst +++ b/docs/configuration/exporting/send-events/alertmanager.rst @@ -13,7 +13,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=alertmanager&account_id= @@ -22,7 +22,7 @@ Configure AlertManager Add a webhook receiver to ``alertmanager.yml``: -.. code-block:: yaml +.. robusta-code:: yaml receivers: - name: robusta diff --git a/docs/configuration/exporting/send-events/aws-cloudwatch.rst b/docs/configuration/exporting/send-events/aws-cloudwatch.rst index 49db98e71..05d582ba9 100644 --- a/docs/configuration/exporting/send-events/aws-cloudwatch.rst +++ b/docs/configuration/exporting/send-events/aws-cloudwatch.rst @@ -14,7 +14,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=awscloudwatch&account_id= @@ -25,7 +25,7 @@ Recipe 2. Create a Lambda function (Python 3.12) and subscribe it to the ``robusta`` SNS topic. Store the Robusta API key in **Lambda → Configuration → Environment variables** as ``ROBUSTA_API_KEY``. 3. Use the following handler: - .. code-block:: python + .. robusta-code:: python import json import os diff --git a/docs/configuration/exporting/send-events/azure-monitor.rst b/docs/configuration/exporting/send-events/azure-monitor.rst index dea8498b0..6ddcfc3bc 100644 --- a/docs/configuration/exporting/send-events/azure-monitor.rst +++ b/docs/configuration/exporting/send-events/azure-monitor.rst @@ -14,7 +14,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=azure&account_id= @@ -23,7 +23,7 @@ Configure Azure Action Group webhook receivers do not allow custom headers, so authenticate via the URL: -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=azure&account_id=&token= diff --git a/docs/configuration/exporting/send-events/datadog.rst b/docs/configuration/exporting/send-events/datadog.rst index 5ef568485..aaddc57c7 100644 --- a/docs/configuration/exporting/send-events/datadog.rst +++ b/docs/configuration/exporting/send-events/datadog.rst @@ -14,7 +14,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=datadog&account_id= diff --git a/docs/configuration/exporting/send-events/dynatrace.rst b/docs/configuration/exporting/send-events/dynatrace.rst index a19624079..c0fe8c225 100644 --- a/docs/configuration/exporting/send-events/dynatrace.rst +++ b/docs/configuration/exporting/send-events/dynatrace.rst @@ -14,7 +14,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=dynatrace&account_id= diff --git a/docs/configuration/exporting/send-events/gcp-monitoring.rst b/docs/configuration/exporting/send-events/gcp-monitoring.rst index d2b7ae5ec..8845678d1 100644 --- a/docs/configuration/exporting/send-events/gcp-monitoring.rst +++ b/docs/configuration/exporting/send-events/gcp-monitoring.rst @@ -14,7 +14,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=gcp&account_id= @@ -23,7 +23,7 @@ Configure GCP GCP webhook notification channels do not support custom headers in the console, so include the API key in the URL: -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=gcp&account_id=&token= diff --git a/docs/configuration/exporting/send-events/grafana.rst b/docs/configuration/exporting/send-events/grafana.rst index d9692385c..f3cd2b453 100644 --- a/docs/configuration/exporting/send-events/grafana.rst +++ b/docs/configuration/exporting/send-events/grafana.rst @@ -14,7 +14,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=grafana&account_id= diff --git a/docs/configuration/exporting/send-events/nagios.rst b/docs/configuration/exporting/send-events/nagios.rst index a58a565ff..8e1a35412 100644 --- a/docs/configuration/exporting/send-events/nagios.rst +++ b/docs/configuration/exporting/send-events/nagios.rst @@ -14,7 +14,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=nagios&account_id= @@ -37,7 +37,7 @@ Restrict the file to the Nagios user: Define a notification command that references ``$USER20$``: -.. code-block:: +.. robusta-code:: define command { command_name notify-robusta-service diff --git a/docs/configuration/exporting/send-events/newrelic.rst b/docs/configuration/exporting/send-events/newrelic.rst index 3ac1e29d2..cbcdd962a 100644 --- a/docs/configuration/exporting/send-events/newrelic.rst +++ b/docs/configuration/exporting/send-events/newrelic.rst @@ -14,7 +14,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=newrelic&account_id= diff --git a/docs/configuration/exporting/send-events/opsgenie.rst b/docs/configuration/exporting/send-events/opsgenie.rst index 516de1e7d..ac00f4971 100644 --- a/docs/configuration/exporting/send-events/opsgenie.rst +++ b/docs/configuration/exporting/send-events/opsgenie.rst @@ -14,7 +14,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=opsgenie&account_id= diff --git a/docs/configuration/exporting/send-events/pagerduty.rst b/docs/configuration/exporting/send-events/pagerduty.rst index 466d31550..20efad710 100644 --- a/docs/configuration/exporting/send-events/pagerduty.rst +++ b/docs/configuration/exporting/send-events/pagerduty.rst @@ -14,7 +14,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=pagerduty&account_id= diff --git a/docs/configuration/exporting/send-events/sentry.rst b/docs/configuration/exporting/send-events/sentry.rst index 2f11170f6..0b01d2885 100644 --- a/docs/configuration/exporting/send-events/sentry.rst +++ b/docs/configuration/exporting/send-events/sentry.rst @@ -14,7 +14,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=sentry&account_id= diff --git a/docs/configuration/exporting/send-events/solarwinds.rst b/docs/configuration/exporting/send-events/solarwinds.rst index cd2db4e56..5ff02ee86 100644 --- a/docs/configuration/exporting/send-events/solarwinds.rst +++ b/docs/configuration/exporting/send-events/solarwinds.rst @@ -14,7 +14,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=solarwinds&account_id= @@ -23,7 +23,7 @@ Configure SolarWinds SolarWinds does not ship a native bearer-token webhook action. Use the **Execute an external program** alert action to invoke ``curl`` (bundled with Windows 10+ and Windows Server 2019+): -.. code-block:: +.. robusta-code:: curl -sS -X POST -H "Authorization: Bearer " -H "Content-Type: application/json" --data "{ \"alertName\": \"${N=Alerting;M=AlertName}\", \"node\": \"${N=SwisEntity;M=Caption}\", \"severity\": \"${N=Alerting;M=Severity}\", \"message\": \"${N=Alerting;M=AlertMessage}\" }" "https://api.robusta.dev/webhooks?type=alert&origin=solarwinds&account_id=" diff --git a/docs/configuration/exporting/send-events/splunk.rst b/docs/configuration/exporting/send-events/splunk.rst index e02a4fd07..fe6b6702e 100644 --- a/docs/configuration/exporting/send-events/splunk.rst +++ b/docs/configuration/exporting/send-events/splunk.rst @@ -14,7 +14,7 @@ Prerequisites Webhook URL ----------- -.. code-block:: +.. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=splunk&account_id= @@ -26,7 +26,7 @@ Splunk's built-in **Webhook** alert action does not let you set custom headers, 1. Open or create a Splunk saved search and choose **Add Actions → Webhook**. 2. Set the **URL** to the webhook URL above with ``&token=`` appended, so authentication travels with the request: - .. code-block:: + .. robusta-code:: https://api.robusta.dev/webhooks?type=alert&origin=splunk&account_id=&token= diff --git a/docs/configuration/github-actions/holmes-pr-review.rst b/docs/configuration/github-actions/holmes-pr-review.rst index 373189774..818561094 100644 --- a/docs/configuration/github-actions/holmes-pr-review.rst +++ b/docs/configuration/github-actions/holmes-pr-review.rst @@ -69,7 +69,7 @@ Step 4: Add the Workflow File Create a new file in your repository at ``.github/workflows/holmes-pr-review.yaml`` with the following contents: -.. code-block:: yaml +.. robusta-code:: yaml name: Holmes PR review on: diff --git a/docs/configuration/holmesgpt/holmes-chat-api.rst b/docs/configuration/holmesgpt/holmes-chat-api.rst index 2ca935d58..11de7d9a7 100644 --- a/docs/configuration/holmesgpt/holmes-chat-api.rst +++ b/docs/configuration/holmesgpt/holmes-chat-api.rst @@ -64,7 +64,7 @@ Example Request The following ``curl`` command demonstrates how to ask Holmes about a failing pod: -.. code-block:: bash +.. robusta-code:: bash curl -X POST 'https://api.robusta.dev/api/holmes/ACCOUNT_ID/chat' \ --header 'Content-Type: application/json' \ @@ -211,7 +211,7 @@ To enable streaming, set ``stream`` to ``true`` in the request body. All other p Example Streaming Request ^^^^^^^^^^^^^^^^^^^^^^^^^ -.. code-block:: bash +.. robusta-code:: bash curl -N -X POST 'https://api.robusta.dev/api/holmes/ACCOUNT_ID/chat' \ --header 'Content-Type: application/json' \ @@ -499,7 +499,7 @@ Consuming the Stream **Python (using requests):** -.. code-block:: python +.. robusta-code:: python import requests import json diff --git a/docs/configuration/resource-recommender.rst b/docs/configuration/resource-recommender.rst index 0069400a1..dac56a709 100644 --- a/docs/configuration/resource-recommender.rst +++ b/docs/configuration/resource-recommender.rst @@ -227,7 +227,7 @@ Retrieves KRR resource recommendations for a specific cluster and namespace. **Example Request** -.. code-block:: bash +.. robusta-code:: bash curl -X GET "https://api.robusta.dev/api/krr/recommendations?account_id=YOUR_ACCOUNT_ID&cluster_id=my-cluster&namespace=default" \ -H "Authorization: Bearer YOUR_API_KEY" \ diff --git a/docs/configuration/sinks/slack.rst b/docs/configuration/sinks/slack.rst index 6cbbc2567..7de6b179c 100644 --- a/docs/configuration/sinks/slack.rst +++ b/docs/configuration/sinks/slack.rst @@ -108,7 +108,7 @@ Set the ``SLACK_FORWARD_URL`` environment variable on the Robusta Runner pod to Add the following to your ``values.yaml`` file and upgrade: -.. code-block:: yaml +.. robusta-code:: yaml runner: additional_env_vars: diff --git a/docs/configuration/sinks/webhook.rst b/docs/configuration/sinks/webhook.rst index 0bb9b42ce..f1ba780a2 100644 --- a/docs/configuration/sinks/webhook.rst +++ b/docs/configuration/sinks/webhook.rst @@ -58,7 +58,7 @@ JSON payload When ``format: json`` is set, the POST body is a JSON object with the following top-level fields: -.. code-block:: json +.. robusta-code:: json { "title": "CrashLoopBackOff", diff --git a/docs/playbook-reference/triggers/elasticsearch.rst b/docs/playbook-reference/triggers/elasticsearch.rst index 1268bc68b..57ba1fd26 100644 --- a/docs/playbook-reference/triggers/elasticsearch.rst +++ b/docs/playbook-reference/triggers/elasticsearch.rst @@ -28,7 +28,7 @@ The following Elasticsearch Watcher configuration will trigger a Robusta playboo Make sure you update ````, ````, and ```` in the emphasized line. These should match the Robusta Helm chart values. -.. code-block:: json +.. robusta-code:: json :emphasize-lines: 26,27,33 { diff --git a/docs/setup-robusta/proxies.rst b/docs/setup-robusta/proxies.rst index 486ed6a11..563ec17aa 100644 --- a/docs/setup-robusta/proxies.rst +++ b/docs/setup-robusta/proxies.rst @@ -36,7 +36,7 @@ When deploying Robusta in a tightly restricted environment, the runner needs out .. note:: Traffic is **always initiated outbound from the runner**. No inbound connections to your cluster are required. All endpoints are reached over HTTPS (TCP/443) unless noted otherwise. -.. code-block:: text +.. robusta-code:: text # Robusta SaaS platform (required if robusta_sink enabled) *.robusta.dev @@ -118,7 +118,7 @@ Verifying the Allowlist After applying firewall rules, you can sanity-check connectivity from inside the runner pod: -.. code-block:: bash +.. robusta-code:: bash kubectl exec -n deploy/robusta-runner -- \ sh -c 'for host in api.robusta.dev relay.robusta.dev platform.robusta.dev; do From b9e918a2bd5618925780d6e5097bff4680277a89 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 12:11:35 +0000 Subject: [PATCH 06/11] Stop uppercasing the 'Select Region' label --- docs/_static/custom.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 9c1b1bb06..501112010 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -378,8 +378,6 @@ h3 { .md-typeset .robusta-region-box__bar-label, .robusta-region-box__bar-label { font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; color: var(--md-default-fg-color--light, rgba(0, 0, 0, 0.6)); } From 70f9736c720b6061622de40fe0a4a4bc1bcf5d91 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 12:18:26 +0000 Subject: [PATCH 07/11] Add :robusta-url: inline role and migrate remaining inline-only docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces an inline counterpart to the .. robusta-url:: / .. robusta-code:: directives, so prose mentions of Robusta URLs get their own region picker without needing a block-level component: Sign up at :robusta-url:`https://platform.robusta.dev/signup` today. Or with custom link text: Visit :robusta-url:`our platform `. The role emits a span.robusta-region-inline wrapping the link; the client-side script appends a compact US/EU/AP picker that shares state with every other region selector on the page (boxes and inline alike). Migrated 15 docs whose only Robusta URL mention was an inline external link or inline literal — signup CTAs in install / architecture / playbook / pro-features / metric-providers / RobustaUI / oss-vs-saas / help / routing-with-scopes / _see_robusta_in_action-2 and the integration endpoint references in alertmanager-integration/{dynatrace, gcp-monitoring, launchdarkly, newrelic}.rst — all now use the role and display the region picker beside the link. The sphinx_design .. button-link:: on the docs index page is left as a plain URL; the page-wide JS still rewrites its href silently so the CTA points at the user's chosen region. --- docs/_ext/region_box.py | 48 ++++++++-- docs/_static/custom.css | 89 +++++++++++++++++++ docs/_static/region-selector.js | 37 ++++++++ .../alertmanager-integration/dynatrace.rst | 2 +- .../gcp-monitoring.rst | 2 +- .../alertmanager-integration/launchdarkly.rst | 4 +- .../alertmanager-integration/newrelic.rst | 2 +- .../exporting/robusta-pro-features.rst | 2 +- .../metric-providers-external.rst | 2 +- docs/configuration/sinks/RobustaUI.rst | 2 +- docs/help.rst | 2 +- docs/how-it-works/architecture.rst | 2 +- docs/how-it-works/oss-vs-saas.rst | 2 +- .../routing-with-scopes.rst | 2 +- docs/playbook-reference/index.rst | 2 +- .../installation/_see_robusta_in_action-2.rst | 2 +- docs/setup-robusta/installation/index.rst | 2 +- 17 files changed, 184 insertions(+), 20 deletions(-) diff --git a/docs/_ext/region_box.py b/docs/_ext/region_box.py index 22ab1e854..9e1bf39d5 100644 --- a/docs/_ext/region_box.py +++ b/docs/_ext/region_box.py @@ -1,5 +1,6 @@ """ -Directives for Robusta platform URL / code blocks with an embedded region selector. +Directives and role for Robusta platform URL / code components with an +embedded region selector. Usage in .rst:: @@ -10,17 +11,28 @@ curl --location --request POST 'https://api.robusta.dev/api/alerts' \ --header 'Authorization: Bearer API-KEY' -The rendered HTML carries the ``robusta-region-box`` class; the -client-side script in ``_static/region-selector.js`` injects the -US/EU/AP selector and keeps every box on the page in sync. + Sign up at :robusta-url:`https://platform.robusta.dev/signup` to get started. + + Or with custom link text: + + Visit :robusta-url:`our platform ` today. + +The rendered HTML carries ``robusta-region-box`` (block) or +``robusta-region-inline`` (inline) classes; the client-side script in +``_static/region-selector.js`` injects the US/EU/AP selector and keeps +every region-aware component on the page in sync. """ +import re from html import escape from docutils import nodes from sphinx.util.docutils import SphinxDirective +_LABELLED_URL_RE = re.compile(r"^\s*(.+?)\s*<\s*([^<>\s]+)\s*>\s*$", re.DOTALL) + + class RobustaUrlDirective(SphinxDirective): has_content = True required_arguments = 0 @@ -75,11 +87,37 @@ def run(self): return [container] +def robusta_url_role(name, rawtext, text, lineno, inliner, options=None, content=None): + """Inline ``:robusta-url:`URL``` or ``:robusta-url:`label ```.""" + raw_text = nodes.unescape(text) + match = _LABELLED_URL_RE.match(raw_text) + if match: + label = match.group(1).strip() + url = match.group(2).strip() + else: + url = raw_text.strip() + label = url + if not url: + msg = inliner.reporter.error( + "robusta-url role requires a URL", line=lineno + ) + return [inliner.problematic(rawtext, rawtext, msg)], [msg] + + html = ( + f'' + f'' + f"{escape(label)}" + f"" + ) + return [nodes.raw("", html, format="html")], [] + + def setup(app): app.add_directive("robusta-url", RobustaUrlDirective) app.add_directive("robusta-code", RobustaCodeDirective) + app.add_role("robusta-url", robusta_url_role) return { - "version": "0.1", + "version": "0.2", "parallel_read_safe": True, "parallel_write_safe": True, } diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 501112010..b5b5721b6 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -479,3 +479,92 @@ h3 { border: 0 !important; border-radius: 0 !important; } + +/* Inline region-aware URL: small picker beside the link */ +.md-typeset .robusta-region-inline, +.robusta-region-inline { + display: inline-flex; + align-items: baseline; + gap: 0.3rem; + white-space: nowrap; + max-width: 100%; +} + +.md-typeset .robusta-region-inline__url, +.robusta-region-inline__url { + word-break: break-all; + white-space: normal; +} + +.md-typeset .robusta-region-inline__picker, +.robusta-region-inline__picker { + display: inline-flex !important; + align-items: center; + vertical-align: baseline; + border-radius: 3px; + overflow: hidden; + line-height: 1; + font-size: 0.62rem; + font-weight: 600; + white-space: nowrap; +} + +.md-typeset .robusta-region-inline__btn, +.robusta-region-inline__btn { + -webkit-appearance: none; + appearance: none; + display: inline-block !important; + border: 1px solid var(--md-default-fg-color--lighter, rgba(0, 0, 0, 0.26)) !important; + background: var(--md-default-bg-color, #fff) !important; + color: var(--md-default-fg-color--light, rgba(0, 0, 0, 0.6)) !important; + padding: 0.05rem 0.35rem !important; + margin: 0 !important; + cursor: pointer; + font: inherit; + font-size: 0.62rem !important; + font-weight: 600; + line-height: 1.2; + text-align: center; + box-shadow: none; + border-radius: 0 !important; + transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease; +} + +.md-typeset .robusta-region-inline__btn + .robusta-region-inline__btn, +.robusta-region-inline__btn + .robusta-region-inline__btn { + border-left-width: 0 !important; +} + +.md-typeset .robusta-region-inline__btn:first-child, +.robusta-region-inline__btn:first-child { + border-top-left-radius: 3px !important; + border-bottom-left-radius: 3px !important; +} + +.md-typeset .robusta-region-inline__btn:last-child, +.robusta-region-inline__btn:last-child { + border-top-right-radius: 3px !important; + border-bottom-right-radius: 3px !important; +} + +.md-typeset .robusta-region-inline__btn:hover, +.robusta-region-inline__btn:hover { + color: var(--md-primary-fg-color, #1976d2) !important; +} + +.md-typeset .robusta-region-inline__btn.is-active, +.robusta-region-inline__btn.is-active { + background: var(--md-primary-fg-color, #1976d2) !important; + border-color: var(--md-primary-fg-color, #1976d2) !important; + color: var(--md-primary-bg-color, #fff) !important; + z-index: 1; + position: relative; +} + +.md-typeset .robusta-region-inline__btn:focus-visible, +.robusta-region-inline__btn:focus-visible { + outline: 2px solid var(--md-accent-fg-color, #1976d2); + outline-offset: 2px; + z-index: 1; + position: relative; +} diff --git a/docs/_static/region-selector.js b/docs/_static/region-selector.js index b650de871..7f63bb00e 100644 --- a/docs/_static/region-selector.js +++ b/docs/_static/region-selector.js @@ -11,6 +11,8 @@ const DETECT_PATTERN = /\b(?:platform|api)(?:\.(?:eu|ap))?\.robusta\.dev\b/; const BAR_CLASS = "robusta-region-box__bar"; const BTN_CLASS = "robusta-region-box__region-btn"; + const INLINE_PICKER_CLASS = "robusta-region-inline__picker"; + const INLINE_BTN_CLASS = "robusta-region-inline__btn"; function getRegion() { try { @@ -95,6 +97,33 @@ applyRegion(regionKey); } + function buildInlinePicker(currentRegion) { + const picker = document.createElement("span"); + picker.className = INLINE_PICKER_CLASS; + picker.setAttribute("role", "radiogroup"); + picker.setAttribute("aria-label", "Select your Robusta region"); + + const buttons = []; + Object.keys(REGIONS).forEach(function (key) { + const btn = document.createElement("button"); + btn.type = "button"; + btn.setAttribute("role", "radio"); + btn.setAttribute("data-region", key); + btn.className = INLINE_BTN_CLASS; + btn.textContent = REGIONS[key].label; + const active = key === currentRegion; + btn.setAttribute("aria-checked", String(active)); + if (active) btn.classList.add("is-active"); + btn.addEventListener("click", function (e) { + e.preventDefault(); + syncAll(key); + }); + picker.appendChild(btn); + buttons.push(btn); + }); + return { picker: picker, buttons: buttons }; + } + function buildBar(currentRegion) { const bar = document.createElement("div"); bar.className = BAR_CLASS; @@ -152,6 +181,14 @@ boxes.push({ buttons: built.buttons }); }); + content.querySelectorAll(".robusta-region-inline").forEach(function (el) { + const existingPicker = el.querySelector(":scope > ." + INLINE_PICKER_CLASS); + if (existingPicker) existingPicker.remove(); + const built = buildInlinePicker(region); + el.appendChild(built.picker); + boxes.push({ buttons: built.buttons }); + }); + collectTargets(content); if (targets.length === 0 && boxes.length === 0) return; diff --git a/docs/configuration/alertmanager-integration/dynatrace.rst b/docs/configuration/alertmanager-integration/dynatrace.rst index a9d640165..57e6a693d 100644 --- a/docs/configuration/alertmanager-integration/dynatrace.rst +++ b/docs/configuration/alertmanager-integration/dynatrace.rst @@ -33,7 +33,7 @@ Step 2: Create a Dynatrace Problems Webhook 3. Click **Add notification** and choose **Webhook**. 4. Configure the **URL**: - ``https://api.robusta.dev/integrations/generic/dynatrace`` + :robusta-url:`https://api.robusta.dev/integrations/generic/dynatrace` 5. Set the **Custom payload** to the Dynatrace macro: diff --git a/docs/configuration/alertmanager-integration/gcp-monitoring.rst b/docs/configuration/alertmanager-integration/gcp-monitoring.rst index 78b737bc3..9482b69b8 100644 --- a/docs/configuration/alertmanager-integration/gcp-monitoring.rst +++ b/docs/configuration/alertmanager-integration/gcp-monitoring.rst @@ -37,7 +37,7 @@ Step 2: Create a Webhook Notification Channel in GCP 3. Configure the webhook with the following settings: - **Display Name**: ``RobustaWebhook`` - - **Endpoint URL**: ``https://api.robusta.dev/integrations/generic/gcp`` + - **Endpoint URL**: :robusta-url:`https://api.robusta.dev/integrations/generic/gcp` - **Authentication**: Select **Basic Authentication** - **Username**: Your Robusta ``account_id`` from Step 1 - **Password**: Your Robusta API key from Step 1 diff --git a/docs/configuration/alertmanager-integration/launchdarkly.rst b/docs/configuration/alertmanager-integration/launchdarkly.rst index 2bd3b4633..b1420129d 100644 --- a/docs/configuration/alertmanager-integration/launchdarkly.rst +++ b/docs/configuration/alertmanager-integration/launchdarkly.rst @@ -43,7 +43,7 @@ In LaunchDarkly: 3. Configure: - **Name**: ``Robusta`` - - **URL**: ``https://api.robusta.dev/integrations/generic/launchdarkly?api_key=YOUR_API_KEY_HERE&account_id=YOUR_ACCOUNT_ID_HERE`` + - **URL**: :robusta-url:`https://api.robusta.dev/integrations/generic/launchdarkly?api_key=YOUR_API_KEY_HERE&account_id=YOUR_ACCOUNT_ID_HERE` - Replace ``YOUR_API_KEY_HERE`` with the API key from Step 1. - Replace ``YOUR_ACCOUNT_ID_HERE`` with your account ID from Step 1. @@ -64,7 +64,7 @@ Including API keys in URLs can expose them in logs, browser history, and monitor If you’re using a third-party service that supports custom headers, configure the webhook like this: -- **URL**: ``https://api.robusta.dev/integrations/generic/launchdarkly?account_id=YOUR_ACCOUNT_ID_HERE`` +- **URL**: :robusta-url:`https://api.robusta.dev/integrations/generic/launchdarkly?account_id=YOUR_ACCOUNT_ID_HERE` - **Headers**: .. code-block:: text diff --git a/docs/configuration/alertmanager-integration/newrelic.rst b/docs/configuration/alertmanager-integration/newrelic.rst index 3de168969..96bfcdadb 100644 --- a/docs/configuration/alertmanager-integration/newrelic.rst +++ b/docs/configuration/alertmanager-integration/newrelic.rst @@ -44,7 +44,7 @@ In New Relic: 2. Click **New destination** → choose **Webhook**. 3. Configure: - - **URL**: ``https://api.robusta.dev/integrations/generic/newrelic`` + - **URL**: :robusta-url:`https://api.robusta.dev/integrations/generic/newrelic` - **Authentication**: **Bearer token** - **Token**: paste the **Robusta API key** from Step 1. diff --git a/docs/configuration/exporting/robusta-pro-features.rst b/docs/configuration/exporting/robusta-pro-features.rst index ae1b3404f..466e23e53 100644 --- a/docs/configuration/exporting/robusta-pro-features.rst +++ b/docs/configuration/exporting/robusta-pro-features.rst @@ -25,5 +25,5 @@ Getting Started To access these APIs: -1. `Sign up `_ for Robusta SaaS or contact support@robusta.dev for self-hosted plans +1. :robusta-url:`Sign up ` for Robusta SaaS or contact support@robusta.dev for self-hosted plans 2. Generate API keys in the Robusta Platform under **Settings** → **API Keys** diff --git a/docs/configuration/metric-providers-external.rst b/docs/configuration/metric-providers-external.rst index 334224961..518679289 100644 --- a/docs/configuration/metric-providers-external.rst +++ b/docs/configuration/metric-providers-external.rst @@ -185,5 +185,5 @@ Next Steps ---------- - Configure :doc:`alert routing ` -- `Set up AI-powered insights `_ +- :robusta-url:`Set up AI-powered insights ` - Learn about :doc:`common configuration options ` \ No newline at end of file diff --git a/docs/configuration/sinks/RobustaUI.rst b/docs/configuration/sinks/RobustaUI.rst index 440840712..136780064 100644 --- a/docs/configuration/sinks/RobustaUI.rst +++ b/docs/configuration/sinks/RobustaUI.rst @@ -20,7 +20,7 @@ Configuring the Robusta UI Sink ------------------------------------------------ .. tip:: - This guide is for users who have already installed Robusta on their cluster. If you haven't installed Robusta yet, we recommend starting by `creating a free Robusta UI account ↗ `_ instead. + This guide is for users who have already installed Robusta on their cluster. If you haven't installed Robusta yet, we recommend starting by :robusta-url:`creating a free Robusta UI account ↗ ` instead. Use the ``robusta`` CLI to generate a token: diff --git a/docs/help.rst b/docs/help.rst index ae03332ef..5de4072ca 100644 --- a/docs/help.rst +++ b/docs/help.rst @@ -260,7 +260,7 @@ Alert Manager is not working .. tip:: - If you're using the Robusta UI, you can test alert routing by `Simulating an alert `_. + If you're using the Robusta UI, you can test alert routing by :robusta-url:`Simulating an alert `. diff --git a/docs/how-it-works/architecture.rst b/docs/how-it-works/architecture.rst index 696535dc9..0cef5e512 100644 --- a/docs/how-it-works/architecture.rst +++ b/docs/how-it-works/architecture.rst @@ -48,7 +48,7 @@ Security & Networking Next Steps ^^^^^^^^^^ -`Ready to install Robusta? Get started. `_ +:robusta-url:`Ready to install Robusta? Get started. ` .. _Robusta Classic: diff --git a/docs/how-it-works/oss-vs-saas.rst b/docs/how-it-works/oss-vs-saas.rst index a7161b4db..b70f3c96f 100644 --- a/docs/how-it-works/oss-vs-saas.rst +++ b/docs/how-it-works/oss-vs-saas.rst @@ -6,7 +6,7 @@ Robusta is delivered through the **Robusta Platform** — a centralized place to SaaS (Hosted) ^^^^^^^^^^^^^^ -The hosted Platform runs in Robusta's infrastructure. You install the in-cluster Agent, sign up at `platform.robusta.dev `_, and your alerts and investigations stream into the hosted UI. +The hosted Platform runs in Robusta's infrastructure. You install the in-cluster Agent, sign up at :robusta-url:`platform.robusta.dev `, and your alerts and investigations stream into the hosted UI. The SaaS Platform is **SOC 2 compliant** and available in multiple regions, including the **US**, **EU**, and **Asia Pacific**. diff --git a/docs/notification-routing/routing-with-scopes.rst b/docs/notification-routing/routing-with-scopes.rst index b74ebd5c1..966be353a 100644 --- a/docs/notification-routing/routing-with-scopes.rst +++ b/docs/notification-routing/routing-with-scopes.rst @@ -277,7 +277,7 @@ Testing Alert Routing ---------------------- .. tip:: - Use the Robusta UI to test your alert routing rules by `simulating an alert `_. + Use the Robusta UI to test your alert routing rules by :robusta-url:`simulating an alert `. Scope Reference ----------------- diff --git a/docs/playbook-reference/index.rst b/docs/playbook-reference/index.rst index c71db435d..241995d30 100644 --- a/docs/playbook-reference/index.rst +++ b/docs/playbook-reference/index.rst @@ -18,7 +18,7 @@ Playbooks Basics Playbooks are deterministic rules for responding to alerts and unhealthy conditions in a Kubernetes cluster. -Playbooks are recommended for advanced use cases. Most users should start with the `Robusta SRE Agent `_, which requires far less configuration. +Playbooks are recommended for advanced use cases. Most users should start with the :robusta-url:`Robusta SRE Agent `, which requires far less configuration. How Playbooks Work ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/setup-robusta/installation/_see_robusta_in_action-2.rst b/docs/setup-robusta/installation/_see_robusta_in_action-2.rst index bf5e3fcf7..af65fe648 100644 --- a/docs/setup-robusta/installation/_see_robusta_in_action-2.rst +++ b/docs/setup-robusta/installation/_see_robusta_in_action-2.rst @@ -55,7 +55,7 @@ Once the pod has reached two restarts, you'll get notified in Slack (or whatever .. image:: /images/crash-report.png -Now open the `Robusta UI `_ and look for the same message there. +Now open the :robusta-url:`Robusta UI ` and look for the same message there. Finally, clean up the crashing pod: diff --git a/docs/setup-robusta/installation/index.rst b/docs/setup-robusta/installation/index.rst index 10ef78996..84796e5ab 100644 --- a/docs/setup-robusta/installation/index.rst +++ b/docs/setup-robusta/installation/index.rst @@ -19,7 +19,7 @@ Prerequisites Sign Up and Install --------------------- -Sign up `for a free Robusta account ↗ `_ and follow the install wizard. It generates a ``generated_values.yaml`` file and provides the Helm install commands tailored to your cluster. +Sign up :robusta-url:`for a free Robusta account ↗ ` and follow the install wizard. It generates a ``generated_values.yaml`` file and provides the Helm install commands tailored to your cluster. .. note:: From 5eb9a49997ce1739432c520a84395b4800276fb6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 12:24:52 +0000 Subject: [PATCH 08/11] Add region picker above the index page Get Started button --- docs/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 7ecbafbfa..453d6c8a9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -77,6 +77,8 @@ Robusta is available as **SaaS**, **self-hosted**, or **open source**. See :doc: Ready to get started? --------------------- +:robusta-url:`https://platform.robusta.dev/signup` + .. button-link:: https://platform.robusta.dev/signup :color: primary :outline: From 8cb8f5aa092ac6d8a7391009540c68975e6a5989 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 12:28:57 +0000 Subject: [PATCH 09/11] Turn the inline region selector into a dropdown menu Replaces the segmented US|EU|AP buttons on each .robusta-region-inline with a single trigger pill (showing the current region + caret) that opens a small dropdown listbox on click. State stays in sync with every other region selector on the page via the existing syncAll() plumbing. The menu closes on outside-click, on Escape, and when another inline picker is opened. Also adds a .. robusta-region-picker:: directive that emits a "Select Region" label plus a standalone dropdown, no URL display. The docs index page now uses this directive above the Get Started button instead of the previous :robusta-url: line, since the button's href is already region-aware and showing the URL again was redundant. --- docs/_ext/region_box.py | 22 +++++- docs/_static/custom.css | 122 +++++++++++++++++++++----------- docs/_static/region-selector.js | 110 ++++++++++++++++++++++------ docs/index.rst | 2 +- 4 files changed, 190 insertions(+), 66 deletions(-) diff --git a/docs/_ext/region_box.py b/docs/_ext/region_box.py index 9e1bf39d5..8a34e1f96 100644 --- a/docs/_ext/region_box.py +++ b/docs/_ext/region_box.py @@ -87,6 +87,25 @@ def run(self): return [container] +class RobustaRegionPickerDirective(SphinxDirective): + """Standalone region picker: a 'Select Region' label + dropdown, no URL.""" + + has_content = False + required_arguments = 0 + optional_arguments = 1 + final_argument_whitespace = True + + def run(self): + label = (self.arguments[0].strip() if self.arguments else "Select Region") + html = ( + f'
' + f'{escape(label)}' + f'' + f"
" + ) + return [nodes.raw("", html, format="html")] + + def robusta_url_role(name, rawtext, text, lineno, inliner, options=None, content=None): """Inline ``:robusta-url:`URL``` or ``:robusta-url:`label ```.""" raw_text = nodes.unescape(text) @@ -115,9 +134,10 @@ def robusta_url_role(name, rawtext, text, lineno, inliner, options=None, content def setup(app): app.add_directive("robusta-url", RobustaUrlDirective) app.add_directive("robusta-code", RobustaCodeDirective) + app.add_directive("robusta-region-picker", RobustaRegionPickerDirective) app.add_role("robusta-url", robusta_url_role) return { - "version": "0.2", + "version": "0.3", "parallel_read_safe": True, "parallel_write_safe": True, } diff --git a/docs/_static/custom.css b/docs/_static/custom.css index b5b5721b6..be5c1352d 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -498,73 +498,113 @@ h3 { .md-typeset .robusta-region-inline__picker, .robusta-region-inline__picker { - display: inline-flex !important; - align-items: center; + position: relative; + display: inline-block !important; vertical-align: baseline; - border-radius: 3px; - overflow: hidden; line-height: 1; - font-size: 0.62rem; + font-size: 0.72rem; font-weight: 600; white-space: nowrap; } -.md-typeset .robusta-region-inline__btn, -.robusta-region-inline__btn { +.md-typeset .robusta-region-inline__trigger, +.robusta-region-inline__trigger { -webkit-appearance: none; appearance: none; - display: inline-block !important; + display: inline-flex !important; + align-items: center; + gap: 0.2rem; border: 1px solid var(--md-default-fg-color--lighter, rgba(0, 0, 0, 0.26)) !important; background: var(--md-default-bg-color, #fff) !important; - color: var(--md-default-fg-color--light, rgba(0, 0, 0, 0.6)) !important; - padding: 0.05rem 0.35rem !important; + color: var(--md-default-fg-color, #333) !important; + padding: 0.1rem 0.45rem !important; margin: 0 !important; cursor: pointer; font: inherit; - font-size: 0.62rem !important; + font-size: 0.72rem !important; font-weight: 600; line-height: 1.2; - text-align: center; + border-radius: 3px !important; box-shadow: none; - border-radius: 0 !important; - transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease; + transition: border-color 0.15s ease, color 0.15s ease; } -.md-typeset .robusta-region-inline__btn + .robusta-region-inline__btn, -.robusta-region-inline__btn + .robusta-region-inline__btn { - border-left-width: 0 !important; +.md-typeset .robusta-region-inline__trigger:hover, +.robusta-region-inline__trigger:hover { + border-color: var(--md-primary-fg-color, #1976d2) !important; + color: var(--md-primary-fg-color, #1976d2) !important; } -.md-typeset .robusta-region-inline__btn:first-child, -.robusta-region-inline__btn:first-child { - border-top-left-radius: 3px !important; - border-bottom-left-radius: 3px !important; +.md-typeset .robusta-region-inline__trigger:focus-visible, +.robusta-region-inline__trigger:focus-visible { + outline: 2px solid var(--md-accent-fg-color, #1976d2); + outline-offset: 2px; } -.md-typeset .robusta-region-inline__btn:last-child, -.robusta-region-inline__btn:last-child { - border-top-right-radius: 3px !important; - border-bottom-right-radius: 3px !important; +.md-typeset .robusta-region-inline__caret, +.robusta-region-inline__caret { + font-size: 0.7em; + line-height: 1; + opacity: 0.7; } -.md-typeset .robusta-region-inline__btn:hover, -.robusta-region-inline__btn:hover { - color: var(--md-primary-fg-color, #1976d2) !important; +.md-typeset .robusta-region-inline__menu, +.robusta-region-inline__menu { + position: absolute; + top: calc(100% + 2px); + left: 0; + z-index: 30; + display: none; + margin: 0 !important; + padding: 0.2rem 0 !important; + list-style: none !important; + min-width: 4rem; + background: var(--md-default-bg-color, #fff); + border: 1px solid var(--md-default-fg-color--lighter, rgba(0, 0, 0, 0.18)); + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); + font-weight: 500; } -.md-typeset .robusta-region-inline__btn.is-active, -.robusta-region-inline__btn.is-active { - background: var(--md-primary-fg-color, #1976d2) !important; - border-color: var(--md-primary-fg-color, #1976d2) !important; - color: var(--md-primary-bg-color, #fff) !important; - z-index: 1; - position: relative; +.md-typeset .robusta-region-inline__picker.is-open .robusta-region-inline__menu, +.robusta-region-inline__picker.is-open .robusta-region-inline__menu { + display: block; } -.md-typeset .robusta-region-inline__btn:focus-visible, -.robusta-region-inline__btn:focus-visible { - outline: 2px solid var(--md-accent-fg-color, #1976d2); - outline-offset: 2px; - z-index: 1; - position: relative; +.md-typeset .robusta-region-inline__option, +.robusta-region-inline__option { + display: block; + padding: 0.3rem 0.75rem; + margin: 0; + cursor: pointer; + color: var(--md-default-fg-color, #333); + user-select: none; +} + +.md-typeset .robusta-region-inline__option:hover, +.robusta-region-inline__option:hover { + background: var(--md-default-fg-color--lightest, rgba(0, 0, 0, 0.06)); +} + +.md-typeset .robusta-region-inline__option.is-active, +.robusta-region-inline__option.is-active { + background: var(--md-primary-fg-color, #1976d2); + color: var(--md-primary-bg-color, #fff); +} + +/* Standalone region picker block (label + dropdown, no URL) */ +.md-typeset .robusta-region-picker, +.robusta-region-picker { + display: inline-flex !important; + align-items: center; + gap: 0.5rem; + margin: 0.75rem 0 0.5rem !important; + font-size: 0.78rem; + line-height: 1.4; +} + +.md-typeset .robusta-region-picker__label, +.robusta-region-picker__label { + font-weight: 600; + color: var(--md-default-fg-color--light, rgba(0, 0, 0, 0.7)); } diff --git a/docs/_static/region-selector.js b/docs/_static/region-selector.js index 7f63bb00e..7bae791a8 100644 --- a/docs/_static/region-selector.js +++ b/docs/_static/region-selector.js @@ -82,12 +82,23 @@ } } for (let j = 0; j < boxes.length; j++) { - const buttons = boxes[j].buttons; - for (let k = 0; k < buttons.length; k++) { - const btn = buttons[k]; - const active = btn.getAttribute("data-region") === regionKey; - btn.classList.toggle("is-active", active); - btn.setAttribute("aria-checked", String(active)); + const box = boxes[j]; + if (box.buttons) { + for (let k = 0; k < box.buttons.length; k++) { + const btn = box.buttons[k]; + const active = btn.getAttribute("data-region") === regionKey; + btn.classList.toggle("is-active", active); + btn.setAttribute("aria-checked", String(active)); + } + } + if (box.items) { + if (box.currentEl) box.currentEl.textContent = REGIONS[regionKey].label; + for (let m = 0; m < box.items.length; m++) { + const item = box.items[m]; + const active = item.getAttribute("data-region") === regionKey; + item.classList.toggle("is-active", active); + item.setAttribute("aria-selected", String(active)); + } } } } @@ -97,31 +108,76 @@ applyRegion(regionKey); } + function closeAllInlineMenus(except) { + document.querySelectorAll("." + INLINE_PICKER_CLASS + ".is-open").forEach(function (p) { + if (p === except) return; + p.classList.remove("is-open"); + const trig = p.querySelector(".robusta-region-inline__trigger"); + if (trig) trig.setAttribute("aria-expanded", "false"); + }); + } + function buildInlinePicker(currentRegion) { const picker = document.createElement("span"); picker.className = INLINE_PICKER_CLASS; - picker.setAttribute("role", "radiogroup"); - picker.setAttribute("aria-label", "Select your Robusta region"); - const buttons = []; + const trigger = document.createElement("button"); + trigger.type = "button"; + trigger.className = "robusta-region-inline__trigger"; + trigger.setAttribute("aria-haspopup", "listbox"); + trigger.setAttribute("aria-expanded", "false"); + + const currentEl = document.createElement("span"); + currentEl.className = "robusta-region-inline__current"; + currentEl.textContent = REGIONS[currentRegion].label; + + const caret = document.createElement("span"); + caret.className = "robusta-region-inline__caret"; + caret.setAttribute("aria-hidden", "true"); + caret.textContent = "▾"; + + trigger.appendChild(currentEl); + trigger.appendChild(caret); + + const menu = document.createElement("ul"); + menu.className = "robusta-region-inline__menu"; + menu.setAttribute("role", "listbox"); + + const items = []; Object.keys(REGIONS).forEach(function (key) { - const btn = document.createElement("button"); - btn.type = "button"; - btn.setAttribute("role", "radio"); - btn.setAttribute("data-region", key); - btn.className = INLINE_BTN_CLASS; - btn.textContent = REGIONS[key].label; - const active = key === currentRegion; - btn.setAttribute("aria-checked", String(active)); - if (active) btn.classList.add("is-active"); - btn.addEventListener("click", function (e) { + const item = document.createElement("li"); + item.setAttribute("role", "option"); + item.setAttribute("data-region", key); + item.className = "robusta-region-inline__option"; + item.tabIndex = -1; + const isActive = key === currentRegion; + item.setAttribute("aria-selected", String(isActive)); + if (isActive) item.classList.add("is-active"); + item.textContent = REGIONS[key].label; + item.addEventListener("click", function (e) { e.preventDefault(); syncAll(key); + picker.classList.remove("is-open"); + trigger.setAttribute("aria-expanded", "false"); + trigger.focus(); }); - picker.appendChild(btn); - buttons.push(btn); + menu.appendChild(item); + items.push(item); }); - return { picker: picker, buttons: buttons }; + + trigger.addEventListener("click", function (e) { + e.preventDefault(); + e.stopPropagation(); + const willOpen = !picker.classList.contains("is-open"); + closeAllInlineMenus(willOpen ? picker : null); + picker.classList.toggle("is-open", willOpen); + trigger.setAttribute("aria-expanded", String(willOpen)); + }); + + picker.appendChild(trigger); + picker.appendChild(menu); + + return { picker: picker, trigger: trigger, currentEl: currentEl, items: items }; } function buildBar(currentRegion) { @@ -186,7 +242,7 @@ if (existingPicker) existingPicker.remove(); const built = buildInlinePicker(region); el.appendChild(built.picker); - boxes.push({ buttons: built.buttons }); + boxes.push({ currentEl: built.currentEl, items: built.items }); }); collectTargets(content); @@ -196,6 +252,14 @@ applyRegion(region); } + document.addEventListener("click", function (e) { + const open = document.querySelector("." + INLINE_PICKER_CLASS + ".is-open"); + if (open && !open.contains(e.target)) closeAllInlineMenus(null); + }); + document.addEventListener("keydown", function (e) { + if (e.key === "Escape") closeAllInlineMenus(null); + }); + if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { diff --git a/docs/index.rst b/docs/index.rst index 453d6c8a9..daebf5e55 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -77,7 +77,7 @@ Robusta is available as **SaaS**, **self-hosted**, or **open source**. See :doc: Ready to get started? --------------------- -:robusta-url:`https://platform.robusta.dev/signup` +.. robusta-region-picker:: .. button-link:: https://platform.robusta.dev/signup :color: primary From 6168baf442e372ba7ac882ec23aa1b7130f3ce10 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 12:31:50 +0000 Subject: [PATCH 10/11] Fix inline dropdown not closing and width misalignment Material's .md-typeset ul styles overrode the menu's display: none, leaving the dropdown permanently visible. Pin display with !important in both the closed and open states. Also tighten the menu to match its trigger: min-width: 100% (of the picker), narrower item padding, and centered labels so the menu sits flush under the AP/EU/US pill instead of mushrooming out to a fixed 4rem width. --- docs/_static/custom.css | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index be5c1352d..c858b5add 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -554,11 +554,12 @@ h3 { top: calc(100% + 2px); left: 0; z-index: 30; - display: none; + display: none !important; margin: 0 !important; - padding: 0.2rem 0 !important; + padding: 0.15rem 0 !important; list-style: none !important; - min-width: 4rem; + min-width: 100%; + width: auto; background: var(--md-default-bg-color, #fff); border: 1px solid var(--md-default-fg-color--lighter, rgba(0, 0, 0, 0.18)); border-radius: 4px; @@ -568,17 +569,19 @@ h3 { .md-typeset .robusta-region-inline__picker.is-open .robusta-region-inline__menu, .robusta-region-inline__picker.is-open .robusta-region-inline__menu { - display: block; + display: block !important; } .md-typeset .robusta-region-inline__option, .robusta-region-inline__option { - display: block; - padding: 0.3rem 0.75rem; - margin: 0; + display: block !important; + padding: 0.25rem 0.5rem !important; + margin: 0 !important; cursor: pointer; color: var(--md-default-fg-color, #333); user-select: none; + text-align: center; + line-height: 1.2; } .md-typeset .robusta-region-inline__option:hover, From 71c3303191c01013c2db4eda5700bf607d8a3153 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 12:44:46 +0000 Subject: [PATCH 11/11] Make inline region dropdown keyboard-operable and harden role parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit region-selector.js: the inline dropdown was mouse-only — every option had tabIndex=-1 and only a click handler. Add a standard listbox keyboard contract: - Tab focuses the trigger; ArrowDown / Enter / Space opens the menu and focuses the active option (ArrowUp opens at the last option) - Inside the open menu: ArrowDown/ArrowUp move focus, Home/End jump to first/last, Enter/Space select, Escape closes and returns focus to the trigger, Tab closes and lets the browser advance - Roving tabindex: the focused option holds tabIndex=0, the rest -1, so the menu participates correctly in the page's tab order - Selection closes the menu, restores focus to the trigger, and flows through the existing syncAll() so every other region selector updates in lock-step region_box.py: defensive fallback in robusta_url_role so an empty-after-strip label can never produce an empty anchor — if the label half ends up blank, fall back to the URL as link text. Today's regex can't actually emit an empty label, but the guard makes a future regex tweak safe. --- docs/_ext/region_box.py | 2 + docs/_static/region-selector.js | 87 +++++++++++++++++++++++++++++---- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/docs/_ext/region_box.py b/docs/_ext/region_box.py index 8a34e1f96..33e7f2bea 100644 --- a/docs/_ext/region_box.py +++ b/docs/_ext/region_box.py @@ -116,6 +116,8 @@ def robusta_url_role(name, rawtext, text, lineno, inliner, options=None, content else: url = raw_text.strip() label = url + if not label: + label = url if not url: msg = inliner.reporter.error( "robusta-url role requires a URL", line=lineno diff --git a/docs/_static/region-selector.js b/docs/_static/region-selector.js index 7bae791a8..7b7ac9ffe 100644 --- a/docs/_static/region-selector.js +++ b/docs/_static/region-selector.js @@ -144,34 +144,103 @@ menu.setAttribute("role", "listbox"); const items = []; + + function focusItem(idx) { + if (idx < 0) idx = items.length - 1; + if (idx >= items.length) idx = 0; + items.forEach(function (it, i) { it.tabIndex = i === idx ? 0 : -1; }); + items[idx].focus(); + } + + function selectItem(key) { + syncAll(key); + picker.classList.remove("is-open"); + trigger.setAttribute("aria-expanded", "false"); + trigger.focus(); + } + Object.keys(REGIONS).forEach(function (key) { const item = document.createElement("li"); item.setAttribute("role", "option"); item.setAttribute("data-region", key); item.className = "robusta-region-inline__option"; - item.tabIndex = -1; const isActive = key === currentRegion; item.setAttribute("aria-selected", String(isActive)); + item.tabIndex = isActive ? 0 : -1; if (isActive) item.classList.add("is-active"); item.textContent = REGIONS[key].label; item.addEventListener("click", function (e) { e.preventDefault(); - syncAll(key); - picker.classList.remove("is-open"); - trigger.setAttribute("aria-expanded", "false"); - trigger.focus(); + selectItem(key); + }); + item.addEventListener("keydown", function (e) { + const idx = items.indexOf(item); + switch (e.key) { + case "Enter": + case " ": + e.preventDefault(); + selectItem(key); + break; + case "ArrowDown": + e.preventDefault(); + focusItem(idx + 1); + break; + case "ArrowUp": + e.preventDefault(); + focusItem(idx - 1); + break; + case "Home": + e.preventDefault(); + focusItem(0); + break; + case "End": + e.preventDefault(); + focusItem(items.length - 1); + break; + case "Tab": + picker.classList.remove("is-open"); + trigger.setAttribute("aria-expanded", "false"); + break; + case "Escape": + e.preventDefault(); + picker.classList.remove("is-open"); + trigger.setAttribute("aria-expanded", "false"); + trigger.focus(); + break; + } }); menu.appendChild(item); items.push(item); }); + function openMenu() { + closeAllInlineMenus(picker); + picker.classList.add("is-open"); + trigger.setAttribute("aria-expanded", "true"); + const activeIdx = items.findIndex(function (it) { return it.classList.contains("is-active"); }); + focusItem(activeIdx >= 0 ? activeIdx : 0); + } + trigger.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); - const willOpen = !picker.classList.contains("is-open"); - closeAllInlineMenus(willOpen ? picker : null); - picker.classList.toggle("is-open", willOpen); - trigger.setAttribute("aria-expanded", String(willOpen)); + if (picker.classList.contains("is-open")) { + picker.classList.remove("is-open"); + trigger.setAttribute("aria-expanded", "false"); + } else { + openMenu(); + } + }); + + trigger.addEventListener("keydown", function (e) { + if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") { + e.preventDefault(); + openMenu(); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + openMenu(); + focusItem(items.length - 1); + } }); picker.appendChild(trigger);