AppPage
uses node:fs
An app (name, what tcm dev is pointed at), and what it's made of (appParts): its code in the browser, the
server functions the browser calls, the tables they reach, its recurring jobs and its tests; whether it's running
(where, for how long, its output), to open, stop or start. Made again every few seconds while it's shown.
import { liveRunning, shownUptime } from "#codebase/tcmts/cli/running"; import { at, codeHref, Fold, Place, servedCodebase, shareNow } from "#codebase/tcmts/share"; import { appLog, appParts, listTests, Live, RunTestsButton, StartAppButton, StopButton, studioApps, StudioFrame, studioShare, } from "#codebase/tcmts/studio"; import fs from "node:fs"; export async function AppPage({ name }: { name: string }) { studioShare(); const s = shareNow(); if (!s.signedIn) return ( <StudioFrame tab="processes" title="Sign in"> {null} </StudioFrame> ); const cb = servedCodebase().cb; const own = s.cb === cb; const placement = s.cache.placement(cb); const app = s.cache .atHead(cb, "studio apps", () => studioApps(cb, placement)) .find((a) => a.path === name); if (!app) return ( <StudioFrame tab="processes" title={name}> <div className="empty">{`${name} isn't an app: tcm dev runs a browser entry point nothing calls.`}</div> </StudioFrame> ); const parts = s.cache.atHead(cb, `studio app ${app.hash}`, () => appParts(cb, placement, app.hash, app.path), ); const running = liveRunning(cb.lay.root).filter((r) => r.app === name); const outcomes = new Map(listTests(cb).map((t) => [t.path, t.ok])); const ns = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : ""; // (a long list: its first dozen, the rest folded under them) const list = <T,>( title: string, items: T[], row: (x: T) => React.ReactNode, empty: string, extra?: React.ReactNode, ) => ( <div className="box"> <div className="boxhead"> <b>{title}</b> <span className="count">{items.length}</span> {extra} </div> {items.length ? items.slice(0, 12).map(row) : <div className="empty">{empty}</div>} {items.length > 12 && ( <div className="tracerest"> <Fold head={<span className="muted">{`${items.length - 12} more`}</span>}> {items.slice(12).map(row)} </Fold> </div> )} </div> ); return ( <StudioFrame tab="processes" title={name}> <Live every={4000} /> <div className="crumbs"> <a href={at("/processes")}>Processes</a> <span className="muted">/</span> </div> <div className="pagehead"> <h2 className="title mono">{name}</h2> <span className="muted"> {running.length ? running .map((r) => `tcm ${r.command}, ${shownUptime(Date.now() - r.started)}`) .join("; ") : "not running"} </span> <span className="right actions"> {running.map((r) => ( <span className="actions" key={r.pid}> {r.url && ( <a className="btn small" href={r.url} target="_blank" rel="noreferrer"> Open </a> )} {r.command === "dev" && fs.existsSync(appLog(cb, name) ?? "") && ( <a className="btn small" href={at(`/processes/output?app=${encodeURIComponent(name)}`)} > Output </a> )} <StopButton pid={r.pid} /> </span> ))} {!running.length && own && <StartAppButton name={name} />} </span> </div> {parts.errors.length > 0 && ( <div className="notice" style={{ borderColor: "var(--bad)", background: "var(--badbg)", marginBottom: 16 }} > <b className="bad">Its plan has problems:</b> {parts.errors.join("; ")} </div> )} <div className="cols-2" style={{ display: "grid", gap: 24 }}> <div> {list( "Server functions", parts.endpoints, (e) => ( <div className="drow full" key={e.path}> <Place place="server" /> <a className="mono" href={codeHref(e.path)}> {e.path} </a> <span className="sig">{e.signature}</span> </div> ), "None: it calls nothing on the server.", <span className="muted">what its browser code calls</span>, )} {list( "In the browser", parts.browser, (b) => ( <div className="drow full" key={b.path}> <Place place={b.place} /> <a className="mono" href={codeHref(b.path)}> {b.path} </a> </div> ), "Nothing of its own.", )} {list( "Tests", parts.tests, (t) => ( <div className="lrow" key={t}> <div> {outcomes.get(t) === undefined ? ( <span className="muted" title="not run as it is now"> ○ </span> ) : outcomes.get(t) ? ( <span className="ok" title="passes"> ✓ </span> ) : ( <span className="bad" title="fails"> ✗ </span> )}{" "} <a className="mono" href={codeHref(t)}> {t} </a> </div> </div> ), "None in its namespace.", own && parts.tests.length > 0 ? ( <span className="right"> <RunTestsButton names={parts.tests} label="Run them" /> </span> ) : undefined, )} </div> <aside className="side"> <div className="sect"> <h4>Its data</h4> {parts.tables.length ? ( parts.tables.map((t) => ( <div key={t.path}> <a className="mono" href={at(`/data/${encodeURIComponent(t.table)}`)}> {t.table} </a>{" "} <span className="muted">{`${t.rows} row${t.rows === 1 ? "" : "s"}`}</span> </div> )) ) : ( <span className="muted">No tables its server code reaches.</span> )} </div> <div className="sect"> <h4>Recurring</h4> {parts.schedules.length ? ( parts.schedules.map((x) => ( <div key={x.path}> <a className="mono" href={codeHref(x.path)}> {x.path} </a>{" "} {x.spec && <span className="muted">{`every ${x.spec}`}</span>} </div> )) ) : ( <span className="muted">None in its namespace.</span> )} {parts.schedules.length > 0 && ( <div style={{ marginTop: 6 }}> <a href={at("/jobs")}>Its jobs</a> </div> )} </div> <div className="sect"> <h4>Its namespace</h4> <a className="mono" href={codeHref(ns)}> {ns || "(the root)"} </a> </div> </aside> </div> </StudioFrame> ); }
uses
- tcmts.cli.running.liveRunning
- tcmts.cli.running.shownUptime
- tcmts.share.Fold
- tcmts.share.Place
- tcmts.share.at
- tcmts.share.codeHref
- tcmts.share.servedCodebase
- tcmts.share.shareNow
- tcmts.studio.Live
- tcmts.studio.RunTestsButton
- tcmts.studio.StartAppButton
- tcmts.studio.StopButton
- tcmts.studio.StudioFrame
- tcmts.studio.appLog
- tcmts.studio.appParts
- tcmts.studio.listTests
- tcmts.studio.studioApps
- tcmts.studio.studioShare