File size: 16,766 Bytes
aef86ea f2db836 aef86ea f2db836 aef86ea f2db836 aef86ea f2db836 aef86ea f2db836 aef86ea f2db836 aef86ea f2db836 aef86ea f2db836 aef86ea f2db836 aef86ea f2db836 aef86ea | 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 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | // ---------------------------------------------------------------------------
// 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>
);
}
/**
* ββ W37-T41 β THE SHAPE OF A SCRIPT, SHOWN AS A PLACEHOLDER RATHER THAN PREFILLED.
*
* Picking Custom View mints a view with an EMPTY source (verified: `script_sandbox.check_source("")`
* accepts it, which is why "mint on pick" was safe to hand lane C in mailbox E-7). Without this, the
* person who has just created one meets a blank box and a Run button, which is not an invitation.
*
* β A PLACEHOLDER, NEVER A PREFILLED `value`. Prefilling would make the view DIRTY the moment it
* opened, arm Save on code nobody wrote, and put a version nobody asked for into a 40-deep history.
* A placeholder vanishes on the first keystroke and is never stored.
* β It is the two lines the sandbox's own fixture uses, so the example a person is shown is the
* example the gate proves runs [[test-double-built-from-the-producer]].
*/
const SCRIPT_STARTER =
"rows = scoped_table()\nemit({'kind': 'kpi', 'label': 'Records', 'value': len(rows)})";
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,
loading = false,
reverting = false,
onRun,
onSave,
onRevert,
}: {
view: ScriptViewRecord | null;
running: boolean;
run: ScriptRun | null;
saving?: boolean;
readOnly?: boolean;
/** β The host knows whether a fetch is outstanding; the panel cannot tell that from a null view.
* Defaulted so no existing caller breaks, which is safe HERE because the default is the older,
* more pessimistic sentence rather than a silently disabled feature. */
loading?: boolean;
reverting?: boolean;
onRun: (draft: string) => void;
onSave?: (source: string) => void;
/**
* β W37-T46 / D-338. Optional for the SAME reason `onSave` is: a read-only reader gets the
* editor without the controls, and a caller that cannot revert simply does not pass this. The
* control is not rendered at all when it is absent, rather than rendered and refused
* [[permitted-is-not-answerable]].
*/
onRevert?: (version: number) => 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;
// β W37-T41: "STILL LOADING" AND "COULD NOT BE OPENED" ARE DIFFERENT FACTS AND USED TO SHARE A
// SENTENCE. With mint-on-pick (E-7) the normal path now passes through `view === null` for as
// long as the fetch takes, so the old copy told every person who created a Custom View that it
// was broken, for a moment, every time. A state that renames itself is exactly what cost this
// product a QA pass one wave ago [[a-store-outage-renames-itself-at-every-layer]].
if (view === null)
return (
<div className="cg-script">
<p className="cg-script-empty cg-script-empty--pad">
{loading === true ? "Opening this view." : "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}`}
{/* β "v5" alone cannot tell a roll-back from a coincidence of matching code. This is
the sentence that makes the history read as what happened. */}
{view.restoredFrom ? `, restored from v${view.restoredFrom}` : ""}
{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>
{/* ββ W37-T41 β THE PLACEHOLDER IS THE CREATE FLOW'S WHOLE EMPTY STATE, and it is the
half that made item 10 worth reopening. Picking Custom View mints a view with an
EMPTY source (verified: `script_sandbox.check_source("")` accepts it, which is why
"mint on pick" was safe to hand lane C in mailbox E-7). Without this the person who
just created one meets a blank box and a Run button, which is not an invitation, it
is a puzzle.
β A PLACEHOLDER, NOT A PREFILLED `value`. Prefilling would make the view DIRTY the
moment it opened, arm Save on code nobody wrote, and put a version nobody asked for
into a 40-deep history. It vanishes on the first keystroke and is never saved. */}
{/* ββ W37-T46 / D-338 β THE CONTROL THAT WAS MISSING. The API already returned
`history` newest-first and the header already showed `v<n>`; there was simply no way
to go back, which is the whole of D-338. It matters more now that a chat writes the
code (R2): generation without roll-back means one bad answer costs somebody their
view, and the person cannot tell in advance which answer that will be.
β THE STRIP IS ABSENT, NOT DISABLED, WHEN THERE IS NOTHING TO GO BACK TO. A first
version has no history, and a row of dead controls reads as a broken feature rather
than as an empty one.
β `trimmed` is PRINTED when it is non-zero. A history capped at 40 that silently
showed 40 would let a reader conclude the view was only ever saved 40 times; saying
how many were dropped is the difference between a partial record and a wrong one. */}
{(view.history?.length ?? 0) > 0 ? (
<div className="cg-script-history" aria-label="Earlier versions">
<span className="cg-script-history-lead">Earlier</span>
{[...(view.history ?? [])].map((h) => (
<button
type="button"
key={h.version}
className="cg-script-history-item"
disabled={readOnly || reverting || onRevert === undefined}
title={`Go back to version ${h.version}, saved by ${h.author || "somebody"}`}
onClick={() => onRevert?.(h.version)}
>
{`v${h.version}`}
</button>
))}
{view.trimmed > 0 ? (
<span className="cg-script-history-trimmed">
{`${view.trimmed} older ${view.trimmed === 1 ? "version" : "versions"} dropped`}
</span>
) : null}
{reverting ? <span className="cg-script-history-trimmed">Going back</span> : null}
</div>
) : null}
<textarea
className="cg-script-code"
value={draft}
spellCheck={false}
readOnly={readOnly}
placeholder={SCRIPT_STARTER}
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>
);
});
|