mirror of
https://github.com/netbirdio/docs.git
synced 2026-08-25 09:11:26 +02:00
Add HA guide for self-hosted Enterprise (#812)
* render mermaid disgrams * wip: add ha docs * improve high availability guide * fix review * remove nav links and references * remove dead server.authSecret references * fix Redis troubleshooting command using non-existent env var * correct startup-log order
This commit is contained in:
@@ -11,6 +11,7 @@ import clsx from 'clsx'
|
||||
import { create } from 'zustand'
|
||||
|
||||
import { Tag } from '@/components/Tag'
|
||||
import { Mermaid } from '@/components/Mermaid'
|
||||
|
||||
const languageNames = {
|
||||
js: 'JavaScript',
|
||||
@@ -290,6 +291,10 @@ export function Code({ children, ...props }) {
|
||||
export function Pre({ children, ...props }) {
|
||||
let isGrouped = useContext(CodeGroupContext)
|
||||
|
||||
if (props.language === 'mermaid') {
|
||||
return <Mermaid chart={props.code} />
|
||||
}
|
||||
|
||||
if (isGrouped) {
|
||||
return children
|
||||
}
|
||||
|
||||
186
src/components/Mermaid.jsx
Normal file
186
src/components/Mermaid.jsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { useCallback, useEffect, useId, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
let mermaidPromise
|
||||
|
||||
function loadMermaid() {
|
||||
if (!mermaidPromise) {
|
||||
mermaidPromise = import('mermaid').then((m) => m.default)
|
||||
}
|
||||
return mermaidPromise
|
||||
}
|
||||
|
||||
function isDarkMode() {
|
||||
return (
|
||||
typeof document !== 'undefined' &&
|
||||
document.documentElement.classList.contains('dark')
|
||||
)
|
||||
}
|
||||
|
||||
function CloseIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Mermaid({ chart, alt = 'Diagram' }) {
|
||||
let reactId = useId().replace(/[^a-zA-Z0-9]/g, '')
|
||||
let renderCount = useRef(0)
|
||||
let renderRun = useRef(0)
|
||||
let hasRendered = useRef(false)
|
||||
let [svg, setSvg] = useState('')
|
||||
let [error, setError] = useState(null)
|
||||
let [zoomed, setZoomed] = useState(false)
|
||||
let [closing, setClosing] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
let lastTheme = null
|
||||
|
||||
async function render() {
|
||||
let dark = isDarkMode()
|
||||
// Skip redundant re-renders when an unrelated class toggles on <html>.
|
||||
if (lastTheme === dark && hasRendered.current) return
|
||||
lastTheme = dark
|
||||
let run = ++renderRun.current
|
||||
|
||||
try {
|
||||
let mermaid = await loadMermaid()
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: 'strict',
|
||||
theme: dark ? 'dark' : 'default',
|
||||
fontFamily:
|
||||
"ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
||||
})
|
||||
let id = `mermaid-${reactId}-${renderCount.current++}`
|
||||
let { svg: out } = await mermaid.render(id, chart)
|
||||
if (!cancelled && run === renderRun.current) {
|
||||
hasRendered.current = true
|
||||
setError(null)
|
||||
setSvg(out)
|
||||
}
|
||||
} catch (e) {
|
||||
if (!cancelled && run === renderRun.current) {
|
||||
setError(e?.message || String(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render()
|
||||
|
||||
// Re-render when the user toggles light/dark so the diagram matches the theme.
|
||||
let observer = new MutationObserver(() => render())
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
observer.disconnect()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [chart])
|
||||
|
||||
let close = useCallback(() => {
|
||||
setClosing(true)
|
||||
setTimeout(() => {
|
||||
setZoomed(false)
|
||||
setClosing(false)
|
||||
}, 200)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!zoomed) return
|
||||
let onKey = (e) => {
|
||||
if (e.key === 'Escape') close()
|
||||
}
|
||||
document.addEventListener('keydown', onKey)
|
||||
return () => document.removeEventListener('keydown', onKey)
|
||||
}, [zoomed, close])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<pre className="my-6 overflow-x-auto rounded border border-red-500/30 bg-red-50/50 p-4 text-xs text-red-700 dark:bg-red-500/5 dark:text-red-300">
|
||||
{`Mermaid render error: ${error}\n\n${chart}`}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
if (!svg) {
|
||||
return <div className="not-prose my-6" aria-hidden="true" />
|
||||
}
|
||||
|
||||
// Mermaid caps the SVG with `max-width: <px>`; strip it for the zoom copy so the
|
||||
// diagram can scale up to fill the lightbox instead of staying at natural size.
|
||||
let zoomSvg = svg.replace(/max-width:\s*[\d.]+px;?/g, '')
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="not-prose my-6 flex justify-center overflow-x-auto [&_svg]:cursor-zoom-in"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`${alt} — activate to zoom`}
|
||||
onClick={() => setZoomed(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
setZoomed(true)
|
||||
}
|
||||
}}
|
||||
// svg is produced by mermaid from trusted, in-repo diagram source
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
|
||||
{zoomed &&
|
||||
createPortal(
|
||||
<div
|
||||
className={`image-zoom-overlay ${closing ? 'closing' : ''}`}
|
||||
onClick={close}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={`Zoomed ${alt}`}
|
||||
>
|
||||
<button
|
||||
className="image-zoom-close"
|
||||
onClick={close}
|
||||
aria-label="Close zoomed diagram"
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
<div
|
||||
className={`image-zoom-content ${
|
||||
closing ? 'closing' : ''
|
||||
} flex items-center justify-center bg-white dark:bg-zinc-900 [&_svg]:!h-auto [&_svg]:!max-h-full [&_svg]:!w-auto [&_svg]:!max-w-full`}
|
||||
style={{
|
||||
width: '90vw',
|
||||
height: '86vh',
|
||||
maxWidth: '90vw',
|
||||
maxHeight: '86vh',
|
||||
padding: '1rem',
|
||||
cursor: 'zoom-out',
|
||||
}}
|
||||
onClick={close}
|
||||
dangerouslySetInnerHTML={{ __html: zoomSvg }}
|
||||
/>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user