File size: 10,980 Bytes
cf17b22 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | // ---------------------------------------------------------------------------
// customer-grid / ScriptViewPanel.tsx β owner item 6's renderer (W36-T04, contract C3).
//
// β ITS OWN MODULE, AND THAT IS A HARNESS FACT AS WELL AS A TIDINESS ONE. It began inside
// `viewModes.tsx`, where the other display modes live β and that module `require`s `./cells`,
// which imports glide-data-grid, whose CJS build cannot load under this repo's node harness. So
// putting it there made the whole component untestable: the ONE thing this surface must prove is
// that a hostile spec is rendered as text, and that claim can only be made by RENDERING. Here the
// import graph is `react` + `./scriptViews` and nothing else, so `gridUx.test.ts` renders it for
// real and asserts on the HTML a person would receive.
// β The stylesheet is imported by `CustomerGrid.tsx` (the mount) for the same reason: tsc
// preserves a side-effect CSS import into the emitted CommonJS and node dies on it.
// ---------------------------------------------------------------------------
import { memo, useEffect, useRef, useState } from "react";
import { barScale, renderableSpec, specKindName } from "./scriptViews";
import type { DrawableSpec, ScriptRun, ScriptView as ScriptViewRecord } from "./scriptViews";
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// ββ W36-T04 (owner item 6 Β· rulings R3/R10 Β· contract C3) β THE CODE-SCRIPT VIEW
//
// Owner, verbatim: *"Add code script as an interface (database View) so a user can build whatever
// they want through the Agent chat interface. be able to create any dashboard they want. User
// should have the ability to see the code AND the dashboard output of course."* Both halves are
// on screen at once, which is the whole of that sentence.
//
// ββ NO BRANCH OF THIS RENDERER EXECUTES A STRING, AND THAT IS THE TICKET'S OWN `done-when`.
// R10 runs the Python in a SERVER sandbox exactly so the browser never has to trust the result;
// a renderer that then evaluated one would hand back everything the sandbox contains. So there is
// no `dangerouslySetInnerHTML`, no `new Function`, no `eval`, no `href`/`src` taken from a spec,
// and no `style` built from spec data anywhere below. Every value reaching the DOM is a SCALAR
// that `renderableSpec` admitted, rendered as TEXT β the one exception being a bar's width, which
// is a number clamped to a percentage here and never a string from the wire.
//
// β AN UNKNOWN `kind` IS A SENTENCE, NOT A BLANK. `spec` is whatever the script emitted (E's C3
// note 1: there is no fixed vocabulary, deliberately), so a view whose author invented
// `kind: "sankey"` is a legitimate thing this build cannot draw. Saying so, by name, is the
// difference between "this build does not know that shape yet" and "your script is broken".
//
// β `ok: false` IS A 200. A script that timed out or divided by zero asked a well-formed question
// whose ANSWER is that no view was produced; it is rendered beside the code, never thrown, and
// never a toast that disappears while the reader is looking at the line that caused it.
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function SpecTable({ spec }: { spec: DrawableSpec }) {
return (
<table className="cg-script-table">
<thead>
<tr>{(spec.columns ?? []).map((c, i) => <th key={`${c}:${i}`} scope="col">{c}</th>)}</tr>
</thead>
<tbody>
{(spec.rows ?? []).map((row, r) => (
<tr key={r}>{row.map((cell, c) => <td key={c}>{cell}</td>)}</tr>
))}
</tbody>
</table>
);
}
function SpecMetrics({ spec }: { spec: DrawableSpec }) {
return (
<div className="cg-script-metrics">
{(spec.items ?? []).map((item, i) => (
<div className="cg-script-metric" key={`${item.label}:${i}`}>
<div className="cg-script-metric-label">{item.label}</div>
<div className="cg-script-metric-value">{item.value}</div>
{item.note ? <div className="cg-script-metric-note">{item.note}</div> : null}
</div>
))}
</div>
);
}
function SpecBars({ spec }: { spec: DrawableSpec }) {
const series = spec.series ?? [];
const scale = barScale(series);
return (
<div className="cg-script-bars">
{series.map((s, i) => (
<div className="cg-script-bar" key={`${s.label}:${i}`}>
<span>{s.label}</span>
<span className="cg-script-bar-track">
{/* β The ONE computed style on this surface, and it is arithmetic on a NUMBER the
spec reader already validated as finite, clamped here. Never a string from the
wire, which is what would make this an injection point. */}
<span
className="cg-script-bar-fill"
style={{ width: `${Math.min(100, Math.round((Math.abs(s.value) / scale) * 100))}%` }}
/>
</span>
<span className="cg-script-bar-value">{s.display}</span>
</div>
))}
</div>
);
}
/** The refusal, and the four limit fields PRINTED rather than re-worded (C3 note 2). */
function ScriptRefusal({ run }: { run: ScriptRun }) {
return (
<div className="cg-script-refusal" role="status">
<h4>This script did not produce a view</h4>
<p>{run.error ?? "The run ended without emitting anything."}</p>
<dl>
<dt>Reason</dt>
<dd>{run.code}</dd>
{run.limit ? <><dt>Subject</dt><dd>{run.limit.subject}</dd></> : null}
{run.limit ? <><dt>Effect</dt><dd>{run.limit.effect}</dd></> : null}
{run.limit ? <><dt>Cause</dt><dd>{run.limit.cause}</dd></> : null}
{run.limit ? <><dt>What to do</dt><dd>{run.limit.recommendation}</dd></> : null}
</dl>
</div>
);
}
export function ScriptOutput({ run }: { run: ScriptRun | null }) {
if (run === null)
return <p className="cg-script-empty">Run the script to see what it draws.</p>;
const drawable = run.ok ? renderableSpec(run.spec) : null;
const claimed = specKindName(run.spec);
return (
<>
{!run.ok ? <ScriptRefusal run={run} /> : null}
{run.ok && drawable === null ? (
<div className="cg-script-refusal" role="status">
<h4>This build cannot draw that shape yet</h4>
<p>
{claimed
? `The script emitted a view of kind "${claimed}", which this version does not
render. It knows table, metrics, bars and text.`
: `The script emitted a view with no recognisable kind. This version renders
table, metrics, bars and text.`}
</p>
</div>
) : null}
{drawable !== null ? (
<>
{drawable.title ? <h3 className="cg-script-title">{drawable.title}</h3> : null}
{drawable.kind === "table" ? <SpecTable spec={drawable} /> : null}
{drawable.kind === "metrics" ? <SpecMetrics spec={drawable} /> : null}
{drawable.kind === "bars" ? <SpecBars spec={drawable} /> : null}
{drawable.kind === "text" ? <p className="cg-script-text">{drawable.body}</p> : null}
</>
) : null}
{run.stdout ? (
<pre className="cg-script-stdout">
{run.stdout}
{run.truncated ? "\n[output truncated]" : ""}
</pre>
) : null}
</>
);
}
/**
* The whole View: the source on the left, what it drew on the right.
*
* β A DRAFT RUN NEEDS NO SAVE (C3 note 3), which is what makes this usable: type, run, save when
* it works. The Save button is separate and deliberately not automatic, because a PUT is a NEW
* VERSION and an autosaving editor would fill the 40-deep history with keystrokes.
*/
export const ScriptView = memo(function ScriptView({
view,
running,
run,
saving,
readOnly = false,
onRun,
onSave,
}: {
view: ScriptViewRecord | null;
running: boolean;
run: ScriptRun | null;
saving?: boolean;
readOnly?: boolean;
onRun: (draft: string) => void;
onSave?: (source: string) => void;
}) {
const [draft, setDraft] = useState(view?.source ?? "");
const loadedId = useRef<string | null>(null);
useEffect(() => {
// Re-seed only when a DIFFERENT view is opened. Re-seeding on every `view` identity would
// discard what somebody is typing the moment the list refreshes underneath them.
if (!view || loadedId.current === view.id) return;
loadedId.current = view.id;
setDraft(view.source);
}, [view]);
const dirty = view !== null && draft !== view.source;
if (view === null)
return (
<div className="cg-script">
<p className="cg-script-empty cg-script-empty--pad">
This script view could not be opened.
</p>
</div>
);
return (
<div className="cg-script">
<div className="cg-script-body">
<section className="cg-script-pane" aria-label="Script source">
<div className="cg-script-pane-head">
<span>Code</span>
<span className="cg-script-meta">
{`v${view.version}`}
{dirty ? ", unsaved changes" : ""}
</span>
<span className="cg-script-spacer" />
<button
type="button"
className="cg-script-ghost"
disabled={!dirty || saving === true || readOnly || onSave === undefined}
onClick={() => onSave?.(draft)}
>
{saving === true ? "Saving" : "Save version"}
</button>
<button
type="button"
className="cg-script-run"
disabled={running}
onClick={() => onRun(draft)}
>
{running ? "Running" : "Run"}
</button>
</div>
<textarea
className="cg-script-code"
value={draft}
spellCheck={false}
readOnly={readOnly}
aria-label="Script source"
onChange={(e) => setDraft(e.target.value)}
/>
</section>
<section className="cg-script-pane" aria-label="Script output">
<div className="cg-script-pane-head">
<span>Output</span>
<span className="cg-script-spacer" />
{run !== null ? (
<span className="cg-script-meta">
{run.ms > 0 ? `${run.ms} ms` : ""}
</span>
) : null}
</div>
<div className="cg-script-out">
<ScriptOutput run={run} />
</div>
</section>
</div>
</div>
);
});
|