From 46fb9b9d6b044444b690b0b4c3eb0eb20cabc00c Mon Sep 17 00:00:00 2001 From: Sven laptop Date: Thu, 24 Sep 2026 20:24:53 +0200 Subject: [PATCH] Added docviewer for docks on the monitoring UI --- monitoring_ui/src/App.jsx | 161 +++++++++++++++++------- monitoring_ui/src/docs.js | 41 +++++++ monitoring_ui/src/styles.css | 231 +++++++++++++++++++++++++++-------- 3 files changed, 338 insertions(+), 95 deletions(-) create mode 100644 monitoring_ui/src/docs.js diff --git a/monitoring_ui/src/App.jsx b/monitoring_ui/src/App.jsx index 2fcd7b5..fe88653 100644 --- a/monitoring_ui/src/App.jsx +++ b/monitoring_ui/src/App.jsx @@ -1,4 +1,25 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { marked } from "marked"; +import { docGroups, docsIndex, resolveDocRel } from "./docs"; + +marked.use({ + renderer: { + heading({ tokens, depth }) { + const text = this.parser.parseInline(tokens); + const id = slug(text) || "section"; + return `${text}`; + }, + }, +}); + +function slug(text) { + return String(text || "") + .toLowerCase() + .trim() + .replace(/[^\w\s-]/g, "") + .replace(/[\s_]+/g, "-") + .replace(/-+/g, "-"); +} const runtime = typeof window !== "undefined" && window.__ENV__ ? window.__ENV__ : {}; @@ -451,7 +472,7 @@ function App() { )} {activeView === "catalog" && } - {activeView === "docs" && } + {activeView === "docs" && } {activeView === "incidents" && ( )} @@ -560,53 +581,103 @@ function QuickNotes() { ); } -function Docs() { +function DocsViewer() { + const [selected, setSelected] = useState( + () => docsIndex.find((d) => d.group === "(top level)" && d.file === "README.md")?.rel || docsIndex[0]?.rel || "", + ); + const [query, setQuery] = useState(""); + const contentRef = useRef(null); + + const doc = docsIndex.find((d) => d.rel === selected) || docsIndex[0]; + const html = useMemo(() => (doc ? marked.parse(doc.content) : ""), [doc]); + + const needle = query.trim().toLowerCase(); + const visible = needle + ? docsIndex.filter( + (d) => + d.label.toLowerCase().includes(needle) || + d.rel.toLowerCase().includes(needle), + ) + : docsIndex; + const groups = docGroups.filter((g) => visible.some((d) => d.group === g)); + + useEffect(() => { + const el = contentRef.current; + if (!el) return; + const onClick = (event) => { + const anchor = event.target.closest("a"); + if (!anchor) return; + const href = anchor.getAttribute("href"); + if (!href) return; + if (href.startsWith("#")) { + event.preventDefault(); + const target = el.querySelector(href); + if (target) target.scrollIntoView({ behavior: "smooth", block: "start" }); + return; + } + if (/^https?:/i.test(href)) return; + event.preventDefault(); + const resolved = resolveDocRel(doc.rel, href); + if (resolved) setSelected(resolved); + else window.open(new URL(href, window.location.href).href, "_blank", "noopener,noreferrer"); + }; + el.addEventListener("click", onClick); + return () => el.removeEventListener("click", onClick); + }, [doc.rel]); + return ( -
-
-
-

SERVICE DOCUMENTATION

-

API Documentation & References

-
- - {Object.keys(serviceMeta).length} services - -
-
- {Object.entries(serviceMeta).map(([key, meta]) => ( -
-
- -
- {meta.label} -

{meta.role}

-
-
-

{meta.data}

- ))} + +
+
+
+
+

MARKDOWN DOCUMENTATION

+

{doc ? doc.label : "No document selected"}

+
+
+ + {doc ? `docs/${doc.rel}` : ""} + +
+ {doc ? ( +
+ ) : ( +
+ No documents + No markdown files were bundled with the dashboard. +
+ )}
); diff --git a/monitoring_ui/src/docs.js b/monitoring_ui/src/docs.js new file mode 100644 index 0000000..14342d0 --- /dev/null +++ b/monitoring_ui/src/docs.js @@ -0,0 +1,41 @@ +const entries = import.meta.glob("../docs/**/*.md", { + as: "raw", + eager: true, +}); + +export const docsIndex = Object.entries(entries) + .map(([path, content]) => { + const rel = path.replace("../docs/", ""); + const parts = rel.split("/"); + const file = parts[parts.length - 1]; + const group = + parts.length > 1 ? parts.slice(0, -1).join("/") : "(top level)"; + return { + rel, + group, + file, + label: file.replace(/\.md$/, "").replace(/_/g, " "), + content, + }; + }) + .sort((a, b) => a.rel.localeCompare(b.rel)); + +const groups = [...new Set(docsIndex.map((d) => d.group))]; +export const docGroups = [ + "(top level)", + ...groups.filter((g) => g !== "(top level)").sort(), +]; + +export function resolveDocRel(fromRel, href) { + if (!href) return null; + const clean = href.split("#")[0] || ""; + if (!clean.endsWith(".md")) return null; + const dirParts = fromRel.split("/").slice(0, -1); + for (const part of clean.split("/")) { + if (!part || part === ".") continue; + if (part === "..") dirParts.pop(); + else dirParts.push(part); + } + const resolved = dirParts.join("/"); + return docsIndex.some((d) => d.rel === resolved) ? resolved : null; +} \ No newline at end of file diff --git a/monitoring_ui/src/styles.css b/monitoring_ui/src/styles.css index cd26cfb..314e65b 100644 --- a/monitoring_ui/src/styles.css +++ b/monitoring_ui/src/styles.css @@ -647,72 +647,203 @@ textarea:focus { .docs-panel { min-height: 500px; } -.docs-grid { + +.docs-viewer { display: grid; - grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); - gap: 16px; + grid-template-columns: 280px 1fr; + min-height: 560px; + padding: 0; + overflow: hidden; } -.doc-card { - background: rgba(23, 32, 34, 0.88); - border: 1px solid var(--line); - border-radius: 6px; - padding: 20px; - border-top: 2px solid var(--card-color); +.docs-sidebar { + background: #141c1e; + border-right: 1px solid var(--line); + padding: 18px; + max-height: 74vh; + overflow-y: auto; } -.doc-card.amber { --card-color: var(--amber); } -.doc-card.teal { --card-color: var(--mint); } -.doc-card.blue { --card-color: var(--blue); } -.doc-card.coral { --card-color: var(--coral); } -.doc-header { - display: flex; - gap: 12px; - align-items: start; +.docs-search { + width: 100%; + padding: 8px 10px; + font-size: 12px; +} +.docs-group { margin-bottom: 12px; } -.doc-header .service-dot { - flex-shrink: 0; - margin-top: 2px; -} -.doc-header strong { display: block; font-size: 14px; } -.doc-header p { margin: 3px 0 0; color: var(--muted); font-size: 11px; } -.doc-description { +.docs-group-title { + font: 600 10px "DM Mono", monospace; + text-transform: uppercase; + letter-spacing: 0.12em; color: var(--muted); + margin: 18px 0 6px; +} +.docs-group:first-child .docs-group-title { + margin-top: 0; +} +.docs-nav-item { + display: block; + width: 100%; + text-align: left; + background: transparent; + border: 0; + color: #c6d5d3; font-size: 12px; - line-height: 1.5; - margin-bottom: 14px; + padding: 7px 9px; + border-radius: 4px; + cursor: pointer; + line-height: 1.3; } -.doc-links { +.docs-nav-item:hover { + background: #1d282a; + color: var(--mint); +} +.docs-nav-item.active { + background: rgba(138, 232, 194, 0.12); + color: var(--mint); +} +.docs-nav-item small { + display: block; + color: var(--muted); + font: 10px "DM Mono", monospace; +} +.docs-content { + padding: 22px 28px 32px; + min-width: 0; + max-height: 74vh; + overflow-y: auto; +} +.docs-content-head { display: flex; - flex-direction: column; - gap: 12px; + justify-content: space-between; + align-items: baseline; + gap: 14px; + flex-wrap: wrap; + margin-bottom: 10px; } -.doc-link { +.docs-source { + font: 10px "DM Mono", monospace; + color: var(--muted); + white-space: nowrap; +} +.markdown-body { + color: #d3dddd; + font-size: 14px; + line-height: 1.65; + word-wrap: break-word; +} +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4 { + color: #e7eeee; + letter-spacing: -0.02em; + margin: 1.5em 0 0.5em; + scroll-margin-top: 20px; +} +.markdown-body h1 { + font-size: 24px; + padding-bottom: 8px; + border-bottom: 1px solid var(--line); +} +.markdown-body h2 { + font-size: 19px; +} +.markdown-body h3 { + font-size: 15px; +} +.markdown-body p { + margin: 0.7em 0; +} +.markdown-body a { color: var(--mint); text-decoration: none; - font: 500 11px "DM Mono", monospace; - display: inline-flex; - align-items: center; - gap: 6px; } -.doc-link:hover { text-decoration: underline; } -.routes-detail summary { - color: var(--muted); - font: 500 10px "DM Mono", monospace; - cursor: pointer; - text-transform: uppercase; +.markdown-body a:hover { + text-decoration: underline; } -.routes-detail ul { - margin: 8px 0 0; - padding-left: 18px; - font-size: 11px; - color: #c6d5d3; +.markdown-body ul, +.markdown-body ol { + padding-left: 22px; + margin: 0.7em 0; } -.routes-detail li { margin: 4px 0; } -.routes-detail code { - font: 11px "DM Mono", monospace; +.markdown-body li { + margin: 0.32em 0; +} +.markdown-body li > ul, +.markdown-body li > ol { + margin: 0.2em 0; +} +.markdown-body code { + font: 0.9em "DM Mono", monospace; background: #111a1c; - padding: 2px 4px; - border-radius: 2px; + padding: 2px 5px; + border-radius: 3px; + color: var(--amber); +} +.markdown-body pre { + background: #0d1416; + border: 1px solid var(--line); + border-radius: 6px; + padding: 14px 16px; + overflow: auto; + line-height: 1.45; +} +.markdown-body pre code { + background: none; + padding: 0; + color: #d3dddd; + font-size: 12px; +} +.markdown-body blockquote { + margin: 1em 0; + padding: 0.6em 14px; + border-left: 3px solid var(--blue); + background: rgba(126, 183, 245, 0.06); + color: var(--muted); +} +.markdown-body table { + border-collapse: collapse; + width: 100%; + margin: 1em 0; + font-size: 13px; +} +.markdown-body th, +.markdown-body td { + border: 1px solid var(--line); + padding: 8px 10px; + text-align: left; + vertical-align: top; +} +.markdown-body th { + background: #1a2527; + color: #e7eeee; + font-weight: 600; +} +.markdown-body hr { + border: 0; + border-top: 1px solid var(--line); + margin: 1.6em 0; +} +.markdown-body img { + max-width: 100%; + border-radius: 4px; +} +.markdown-body > :first-child { + margin-top: 0; +} +@media (max-width: 900px) { + .docs-viewer { + grid-template-columns: 1fr; + } + .docs-sidebar { + border-right: 0; + border-bottom: 1px solid var(--line); + max-height: 38vh; + } + .docs-content { + max-height: none; + padding: 20px; + } } @media (max-width: 700px) {