AppPage

tcmts.studio.AppPage · #hpifhgon · term · runs on the server

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

used by