Added docviewer for docks on the monitoring UI
Build and Push Docker Images / detect-changes (push) Successful in 29s
Build and Push Docker Images / build-auth-api (push) Successful in 2m1s
Build and Push Docker Images / build-employee-api (push) Successful in 2m1s
Build and Push Docker Images / build-transit-api (push) Successful in 2m9s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m57s
Build and Push Docker Images / build-admin-ui (push) Successful in 2m50s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 2m39s
Build and Push Docker Images / deploy-test-node (push) Successful in 6s
Build and Push Docker Images / detect-changes (push) Successful in 29s
Build and Push Docker Images / build-auth-api (push) Successful in 2m1s
Build and Push Docker Images / build-employee-api (push) Successful in 2m1s
Build and Push Docker Images / build-transit-api (push) Successful in 2m9s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m57s
Build and Push Docker Images / build-admin-ui (push) Successful in 2m50s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 2m39s
Build and Push Docker Images / deploy-test-node (push) Successful in 6s
This commit is contained in:
+114
-43
@@ -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 `<h${depth} id="${id}">${text}</h${depth}>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function slug(text) {
|
||||||
|
return String(text || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/[^\w\s-]/g, "")
|
||||||
|
.replace(/[\s_]+/g, "-")
|
||||||
|
.replace(/-+/g, "-");
|
||||||
|
}
|
||||||
|
|
||||||
const runtime =
|
const runtime =
|
||||||
typeof window !== "undefined" && window.__ENV__ ? window.__ENV__ : {};
|
typeof window !== "undefined" && window.__ENV__ ? window.__ENV__ : {};
|
||||||
@@ -451,7 +472,7 @@ function App() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{activeView === "catalog" && <Catalog />}
|
{activeView === "catalog" && <Catalog />}
|
||||||
{activeView === "docs" && <Docs />}
|
{activeView === "docs" && <DocsViewer />}
|
||||||
{activeView === "incidents" && (
|
{activeView === "incidents" && (
|
||||||
<Incidents errors={errors} state={errorState} reload={loadErrors} />
|
<Incidents errors={errors} state={errorState} reload={loadErrors} />
|
||||||
)}
|
)}
|
||||||
@@ -560,53 +581,103 @@ function QuickNotes() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
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 (
|
return (
|
||||||
<section className="panel docs-panel">
|
<section className="panel docs-viewer">
|
||||||
<div className="panel-heading">
|
<aside className="docs-sidebar">
|
||||||
<div>
|
<input
|
||||||
<p className="eyebrow">SERVICE DOCUMENTATION</p>
|
className="docs-search"
|
||||||
<h2>API Documentation & References</h2>
|
placeholder="Filter docs…"
|
||||||
|
value={query}
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
/>
|
||||||
|
{groups.map((group) => (
|
||||||
|
<div className="docs-group" key={group}>
|
||||||
|
<p className="docs-group-title">
|
||||||
|
{group === "(top level)" ? "General" : group}
|
||||||
|
</p>
|
||||||
|
{visible
|
||||||
|
.filter((d) => d.group === group)
|
||||||
|
.map((d) => (
|
||||||
|
<button
|
||||||
|
key={d.rel}
|
||||||
|
className={`docs-nav-item ${d.rel === doc.rel ? "active" : ""}`}
|
||||||
|
onClick={() => setSelected(d.rel)}
|
||||||
|
>
|
||||||
|
{d.label}
|
||||||
|
<small>{d.file}</small>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<span className="panel-count">
|
))}
|
||||||
{Object.keys(serviceMeta).length} services
|
</aside>
|
||||||
|
<div className="docs-content">
|
||||||
|
<div className="docs-content-head">
|
||||||
|
<div className="panel-heading" style={{ marginBottom: 0 }}>
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">MARKDOWN DOCUMENTATION</p>
|
||||||
|
<h2>{doc ? doc.label : "No document selected"}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="docs-source">
|
||||||
|
{doc ? `docs/${doc.rel}` : ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="docs-grid">
|
{doc ? (
|
||||||
{Object.entries(serviceMeta).map(([key, meta]) => (
|
<article
|
||||||
<div className={`doc-card ${meta.color}`} key={key}>
|
className="markdown-body"
|
||||||
<div className="doc-header">
|
ref={contentRef}
|
||||||
<span className={`service-dot ${meta.color}`} />
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
<div>
|
/>
|
||||||
<strong>{meta.label}</strong>
|
) : (
|
||||||
<p>{meta.role}</p>
|
<div className="empty">
|
||||||
|
<strong>No documents</strong>
|
||||||
|
<span>No markdown files were bundled with the dashboard.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<p className="doc-description">{meta.data}</p>
|
|
||||||
<div className="doc-links">
|
|
||||||
{meta.docs && (
|
|
||||||
<a
|
|
||||||
href={meta.docs}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="doc-link"
|
|
||||||
>
|
|
||||||
View Documentation →
|
|
||||||
</a>
|
|
||||||
)}
|
)}
|
||||||
<details className="routes-detail">
|
|
||||||
<summary>API Routes</summary>
|
|
||||||
<ul>
|
|
||||||
{meta.routes.map((route) => (
|
|
||||||
<li key={route}>
|
|
||||||
<code>{route}</code>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
+181
-50
@@ -647,72 +647,203 @@ textarea:focus {
|
|||||||
.docs-panel {
|
.docs-panel {
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
}
|
}
|
||||||
.docs-grid {
|
|
||||||
|
.docs-viewer {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
grid-template-columns: 280px 1fr;
|
||||||
gap: 16px;
|
min-height: 560px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.doc-card {
|
.docs-sidebar {
|
||||||
background: rgba(23, 32, 34, 0.88);
|
background: #141c1e;
|
||||||
border: 1px solid var(--line);
|
border-right: 1px solid var(--line);
|
||||||
border-radius: 6px;
|
padding: 18px;
|
||||||
padding: 20px;
|
max-height: 74vh;
|
||||||
border-top: 2px solid var(--card-color);
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.doc-card.amber { --card-color: var(--amber); }
|
.docs-search {
|
||||||
.doc-card.teal { --card-color: var(--mint); }
|
width: 100%;
|
||||||
.doc-card.blue { --card-color: var(--blue); }
|
padding: 8px 10px;
|
||||||
.doc-card.coral { --card-color: var(--coral); }
|
font-size: 12px;
|
||||||
.doc-header {
|
}
|
||||||
display: flex;
|
.docs-group {
|
||||||
gap: 12px;
|
|
||||||
align-items: start;
|
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.doc-header .service-dot {
|
.docs-group-title {
|
||||||
flex-shrink: 0;
|
font: 600 10px "DM Mono", monospace;
|
||||||
margin-top: 2px;
|
text-transform: uppercase;
|
||||||
}
|
letter-spacing: 0.12em;
|
||||||
.doc-header strong { display: block; font-size: 14px; }
|
|
||||||
.doc-header p { margin: 3px 0 0; color: var(--muted); font-size: 11px; }
|
|
||||||
.doc-description {
|
|
||||||
color: var(--muted);
|
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;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
padding: 7px 9px;
|
||||||
margin-bottom: 14px;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
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);
|
color: var(--mint);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font: 500 11px "DM Mono", monospace;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
}
|
||||||
.doc-link:hover { text-decoration: underline; }
|
.markdown-body a:hover {
|
||||||
.routes-detail summary {
|
text-decoration: underline;
|
||||||
color: var(--muted);
|
|
||||||
font: 500 10px "DM Mono", monospace;
|
|
||||||
cursor: pointer;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
.routes-detail ul {
|
.markdown-body ul,
|
||||||
margin: 8px 0 0;
|
.markdown-body ol {
|
||||||
padding-left: 18px;
|
padding-left: 22px;
|
||||||
font-size: 11px;
|
margin: 0.7em 0;
|
||||||
color: #c6d5d3;
|
|
||||||
}
|
}
|
||||||
.routes-detail li { margin: 4px 0; }
|
.markdown-body li {
|
||||||
.routes-detail code {
|
margin: 0.32em 0;
|
||||||
font: 11px "DM Mono", monospace;
|
}
|
||||||
|
.markdown-body li > ul,
|
||||||
|
.markdown-body li > ol {
|
||||||
|
margin: 0.2em 0;
|
||||||
|
}
|
||||||
|
.markdown-body code {
|
||||||
|
font: 0.9em "DM Mono", monospace;
|
||||||
background: #111a1c;
|
background: #111a1c;
|
||||||
padding: 2px 4px;
|
padding: 2px 5px;
|
||||||
border-radius: 2px;
|
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) {
|
@media (max-width: 700px) {
|
||||||
|
|||||||
Reference in New Issue
Block a user