@manta-eu/horns-client 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/component-api.d.ts +7 -0
- package/dist/app/coverage-run.js +4 -3
- package/dist/app/globals.css +2 -0
- package/dist/app/home-page.js +6 -4
- package/dist/app/horn-page.js +7 -10
- package/dist/app/horn-studio.js +63 -10
- package/dist/app/horn-view-page.js +13 -9
- package/dist/app/horns-page.js +6 -9
- package/dist/app/new-horn-page.js +52 -10
- package/dist/app/paths.d.ts +15 -10
- package/dist/app/paths.js +18 -29
- package/dist/app/plans-page.d.ts +1 -0
- package/dist/app/plans-page.js +59 -0
- package/dist/app/reference-page.d.ts +1 -0
- package/dist/app/reference-page.js +55 -0
- package/dist/app/routes.js +7 -5
- package/dist/app/rule-page.js +18 -11
- package/dist/app/rules-page.js +12 -12
- package/dist/app/shape-entity-page.js +7 -9
- package/dist/app/shape-zoo-page.js +9 -10
- package/dist/app/shapes-page.js +13 -15
- package/dist/app/showcase-page.js +8 -11
- package/dist/components/app-shell.js +8 -7
- package/dist/components/catalog-card-view.js +4 -5
- package/dist/components/explain-dialog.d.ts +5 -0
- package/dist/components/explain-dialog.js +26 -0
- package/dist/components/plan-runs.js +2 -2
- package/dist/components/read-the-code.d.ts +19 -0
- package/dist/components/read-the-code.js +17 -3
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -3
- package/dist/lib/horn-review.d.ts +7 -0
- package/dist/lib/horn-review.js +15 -0
- package/dist/lib/horn-variables.d.ts +8 -0
- package/dist/lib/horn-variables.js +39 -0
- package/dist/lib/manta/sign-in.d.ts +2 -0
- package/dist/lib/manta/sign-in.js +9 -0
- package/dist/lib/manta/sparql-select.d.ts +6 -2
- package/dist/lib/manta/sparql-select.js +17 -14
- package/dist/lib/reference/fake-sparql.d.ts +4 -0
- package/dist/lib/reference/fake-sparql.js +216 -0
- package/dist/lib/shapes/ldkit-source.d.ts +1 -1
- package/dist/lib/shapes/ldkit-source.js +1 -1
- package/dist/lib/studio-progress.js +3 -1
- package/dist/lib/surface/declarations.d.ts +5 -0
- package/dist/lib/surface/declarations.js +77 -0
- package/dist/lib/written-surface.js +13 -2
- package/dist/reference/supply-chain-360.horn.json +413 -0
- package/dist/standalone/app.d.ts +2 -0
- package/dist/standalone/app.js +57 -0
- package/dist/standalone/client.d.ts +15 -0
- package/dist/{client.js → standalone/client.js} +12 -8
- package/dist/standalone/gates.d.ts +10 -0
- package/dist/standalone/gates.js +15 -0
- package/dist/styles.css +1 -1
- package/dist/vite/component-api.d.ts +8 -0
- package/dist/vite/component-api.js +24 -0
- package/dist/vite/index.d.ts +3 -0
- package/dist/vite/index.js +3 -0
- package/dist/vite/shape-catalog.d.ts +10 -0
- package/dist/vite/shape-catalog.js +35 -0
- package/dist/vite/typecheck-fs.d.ts +6 -0
- package/dist/vite/typecheck-fs.js +68 -0
- package/package.json +58 -20
- package/shape-catalog.d.ts +9 -0
- package/typecheck-fs.d.ts +6 -0
- package/dist/app/session-gate.d.ts +0 -6
- package/dist/app/session-gate.js +0 -19
- package/dist/client.d.ts +0 -10
- package/dist/lib/manta/providers.d.ts +0 -10
- package/dist/lib/manta/providers.js +0 -48
- package/dist/lib/manta/session.d.ts +0 -47
- package/dist/lib/manta/session.js +0 -204
package/dist/app/horn-page.js
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { isPresent } from "@manta-eu/lib/present";
|
|
3
3
|
import { useParams, useSearchParams } from "react-router";
|
|
4
|
+
import { useActiveDatastore } from "@manta-eu/react/providers";
|
|
4
5
|
import { variablesFromSearch } from "@manta-eu/horns/render";
|
|
5
6
|
import { BackLink, DatastoreMissing, ErrorState, Loading } from "../components/states";
|
|
6
7
|
import { hornsPath } from "./paths";
|
|
7
8
|
import { HornStudio } from "./horn-studio";
|
|
8
9
|
import { useSavedHorn } from "./saved-horn";
|
|
9
10
|
import { VariableBar } from "./variable-bar";
|
|
10
|
-
import { MantaSparqlProvider } from "../lib/manta/providers";
|
|
11
|
-
import { useMantaSession } from "../lib/manta/session";
|
|
12
11
|
// A saved Horn, as the Studio's whole screen: the document comes back from the datastore and the snapshot is
|
|
13
12
|
// built from it, so what is on screen is what is stored rather than what a client happened to be holding.
|
|
14
13
|
export function HornPage() {
|
|
15
|
-
const { datastoreId
|
|
16
|
-
const {
|
|
17
|
-
|
|
18
|
-
if (!isPresent(datastore)) {
|
|
14
|
+
const { datastoreId } = useActiveDatastore();
|
|
15
|
+
const { slug } = useParams();
|
|
16
|
+
if (!isPresent(datastoreId)) {
|
|
19
17
|
return (_jsx(Centered, { children: _jsx(DatastoreMissing, {}) }));
|
|
20
18
|
}
|
|
21
|
-
|
|
22
|
-
return (_jsx(MantaSparqlProvider, { datastoreId: datastore.id, children: _jsx(SavedHornPage, { slug: slug ?? "", datastoreId: datastore.id }) }));
|
|
19
|
+
return _jsx(SavedHornPage, { slug: slug ?? "", datastoreId: datastoreId });
|
|
23
20
|
}
|
|
24
21
|
/** Until there is a Horn to put in the Studio, the screen is just this. */
|
|
25
22
|
function Centered({ children }) {
|
|
@@ -29,10 +26,10 @@ function SavedHornPage({ datastoreId, slug }) {
|
|
|
29
26
|
const [search] = useSearchParams();
|
|
30
27
|
const horn = useSavedHorn(datastoreId, slug);
|
|
31
28
|
if (horn.isError) {
|
|
32
|
-
return (_jsxs(Centered, { children: [_jsx(BackLink, { to: hornsPath(
|
|
29
|
+
return (_jsxs(Centered, { children: [_jsx(BackLink, { to: hornsPath(), label: "All Horns" }), _jsx(ErrorState, { title: "That Horn could not be opened", hint: horn.error instanceof Error ? horn.error.message : "It may have been removed." })] }));
|
|
33
30
|
}
|
|
34
31
|
if (!isPresent(horn.data)) {
|
|
35
32
|
return (_jsx(Centered, { children: _jsx(Loading, { label: "Opening the Horn\u2026" }) }));
|
|
36
33
|
}
|
|
37
|
-
return (_jsx(HornStudio, { datastoreId: datastoreId, slug: slug, stored: horn.data.horn, revision: horn.data.revision, backTo: hornsPath(
|
|
34
|
+
return (_jsx(HornStudio, { datastoreId: datastoreId, slug: slug, stored: horn.data.horn, revision: horn.data.revision, backTo: hornsPath(), variables: variablesFromSearch(horn.data.horn, search.toString()), toolbar: _jsx(VariableBar, { horn: horn.data.horn }) }));
|
|
38
35
|
}
|
package/dist/app/horn-studio.js
CHANGED
|
@@ -11,10 +11,10 @@ import { describeHornChange, minViewWidth, visualEditOps } from "@manta-eu/horns
|
|
|
11
11
|
import { HornSnapshotView } from "@manta-eu/horns-kit";
|
|
12
12
|
import { writtenHornView } from "../components/written-horn-view";
|
|
13
13
|
import { catalogCardView } from "../components/catalog-card-view";
|
|
14
|
-
import { ReadTheCode, writtenReviewStates } from "../components/read-the-code";
|
|
14
|
+
import { ReadTheCode, ReadTheCodeDialog, writtenReviewStates } from "../components/read-the-code";
|
|
15
15
|
import { PlanRunDialog, usePlanRuns } from "../components/plan-runs";
|
|
16
16
|
import { sendHornScreenshot } from "../components/rasterize";
|
|
17
|
-
import { reviewHorn } from "../lib/horn-review";
|
|
17
|
+
import { nameOfView, reviewHorn } from "../lib/horn-review";
|
|
18
18
|
import { StudioChat } from "../components/studio-chat";
|
|
19
19
|
import { ErrorState, Loading } from "../components/states";
|
|
20
20
|
import { isRecord } from "@manta-eu/horns";
|
|
@@ -42,12 +42,13 @@ function failureOf(body, status) {
|
|
|
42
42
|
}
|
|
43
43
|
/** How much a card grows or shrinks by: a third of the twelve columns, so the page keeps its rhythm. */
|
|
44
44
|
const widthStep = 4;
|
|
45
|
+
// Trimmed after the cut, not before it: a slug ending in a dash is not one the schema accepts.
|
|
45
46
|
const kebab = (name) => name
|
|
46
47
|
.trim()
|
|
47
48
|
.toLowerCase()
|
|
48
49
|
.replace(/[^a-z0-9]+/g, "-")
|
|
49
|
-
.
|
|
50
|
-
.
|
|
50
|
+
.slice(0, 60)
|
|
51
|
+
.replace(/^-+|-+$/g, "");
|
|
51
52
|
/**
|
|
52
53
|
* The Studio as a whole screen, the way Lovable lays out a project: the conversation down the left, the Horn in
|
|
53
54
|
* a frame on the right, and saving in the bar across the top.
|
|
@@ -87,6 +88,10 @@ export function HornStudio(props) {
|
|
|
87
88
|
// Hashing is async, so the states are held rather than derived: a card shows what was last worked out for
|
|
88
89
|
// the draft it is drawing.
|
|
89
90
|
const [reviewStates, setReviewStates] = useState({});
|
|
91
|
+
// Save does not refuse a Horn whose code nobody read; it offers the reading, then goes on saving.
|
|
92
|
+
const [toRead, setToRead] = useState([]);
|
|
93
|
+
const [savingAfterReading, setSavingAfterReading] = useState(false);
|
|
94
|
+
const [readingTotal, setReadingTotal] = useState(0);
|
|
90
95
|
useEffect(() => {
|
|
91
96
|
let live = true;
|
|
92
97
|
void writtenReviewStates(shown.horn).then((states) => {
|
|
@@ -182,7 +187,8 @@ export function HornStudio(props) {
|
|
|
182
187
|
setProgress([...thinking]);
|
|
183
188
|
review = await reviewHorn({ datastoreId, horn: reply.horn, asked, variables });
|
|
184
189
|
for (const problem of review.problems) {
|
|
185
|
-
|
|
190
|
+
const named = nameOfView(reply.horn, problem.viewId);
|
|
191
|
+
thinking.push({ title: `“${named}” would not draw`, detail: problem.message });
|
|
186
192
|
}
|
|
187
193
|
if (isPresent(review.advice)) {
|
|
188
194
|
const count = review.advice.length;
|
|
@@ -198,7 +204,7 @@ export function HornStudio(props) {
|
|
|
198
204
|
}
|
|
199
205
|
const said = [
|
|
200
206
|
// A view that would not draw is not an opinion; it goes first, and it is a fact the Studio can act on.
|
|
201
|
-
...review.problems.map((problem) => `“${problem.viewId}” does not draw: ${problem.message}`),
|
|
207
|
+
...review.problems.map((problem) => `“${nameOfView(reply.horn, problem.viewId)}” does not draw: ${problem.message}`),
|
|
202
208
|
...review.advice,
|
|
203
209
|
];
|
|
204
210
|
setEntries([
|
|
@@ -350,14 +356,37 @@ export function HornStudio(props) {
|
|
|
350
356
|
setSaveFailure("Give the Horn a title before saving it: its address is made from the title.");
|
|
351
357
|
return;
|
|
352
358
|
}
|
|
359
|
+
// Asked of the Horn being saved rather than the one on screen, which may be an earlier version. The
|
|
360
|
+
// save route refuses unread code, so meeting it here turns a dead end into the reading it asks for.
|
|
361
|
+
const states = await writtenReviewStates(saving.horn);
|
|
362
|
+
const unread = saving.horn.views
|
|
363
|
+
.map((view) => view.id)
|
|
364
|
+
.filter((id) => states[id] === "unread" || states[id] === "stale");
|
|
365
|
+
if (unread.length > 0) {
|
|
366
|
+
setSaveFailure(undefined);
|
|
367
|
+
setToRead(unread);
|
|
368
|
+
setReadingTotal(unread.length);
|
|
369
|
+
setSavingAfterReading(true);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
353
372
|
setSaveState("saving");
|
|
354
373
|
setSaveFailure(undefined);
|
|
355
374
|
try {
|
|
356
|
-
const
|
|
375
|
+
const put = (at) => fetch(`/api/d/${encodeURIComponent(datastoreId)}/horns/${encodeURIComponent(at)}`, {
|
|
357
376
|
method: "PUT",
|
|
358
377
|
headers: { "content-type": "application/json" },
|
|
359
378
|
body: JSON.stringify({ horn: saving.horn, ...(isPresent(baseRevision) ? { baseRevision } : {}) }),
|
|
360
379
|
});
|
|
380
|
+
// A Horn that has never been saved takes its address from its title, and two Horns can be given the
|
|
381
|
+
// same title — every Horn built for a plan nobody named is "Suggested changes". The name is taken, not
|
|
382
|
+
// the Horn: saving under the next one free is what a person means, and a 409 here can mean nothing
|
|
383
|
+
// else, because revising a Horn is what carries a `baseRevision`.
|
|
384
|
+
let at = name;
|
|
385
|
+
let response = await put(at);
|
|
386
|
+
for (let next = 2; response.status === 409 && !isPresent(baseRevision) && next <= 20; next += 1) {
|
|
387
|
+
at = `${name}-${next}`;
|
|
388
|
+
response = await put(at);
|
|
389
|
+
}
|
|
361
390
|
const body = await response.json().catch(() => undefined);
|
|
362
391
|
if (!response.ok) {
|
|
363
392
|
setSaveState("failed");
|
|
@@ -365,13 +394,13 @@ export function HornStudio(props) {
|
|
|
365
394
|
return;
|
|
366
395
|
}
|
|
367
396
|
setBaseRevision(isRecord(body) && typeof body["revision"] === "string" ? body["revision"] : baseRevision);
|
|
368
|
-
setSlug(
|
|
397
|
+
setSlug(at);
|
|
369
398
|
setSavedId(saving.id);
|
|
370
399
|
setSaveState("idle");
|
|
371
400
|
// The showcase leads with a picture of the Horn, and this browser has it drawn already. A saved Horn is
|
|
372
401
|
// saved whether or not the picture was taken, so this cannot fail the save and is never waited on.
|
|
373
402
|
if (isPresent(snapshot.data)) {
|
|
374
|
-
void sendHornScreenshot({ datastoreId, slug:
|
|
403
|
+
void sendHornScreenshot({ datastoreId, slug: at, snapshot: snapshot.data });
|
|
375
404
|
}
|
|
376
405
|
}
|
|
377
406
|
catch (caught) {
|
|
@@ -379,6 +408,13 @@ export function HornStudio(props) {
|
|
|
379
408
|
setSaveFailure(caught instanceof Error ? caught.message : String(caught));
|
|
380
409
|
}
|
|
381
410
|
}
|
|
411
|
+
useEffect(() => {
|
|
412
|
+
if (!savingAfterReading || toRead.length > 0)
|
|
413
|
+
return;
|
|
414
|
+
setSavingAfterReading(false);
|
|
415
|
+
// The queue emptying is the event; `save` is redefined every render and is not one.
|
|
416
|
+
void save();
|
|
417
|
+
}, [savingAfterReading, toRead]);
|
|
382
418
|
// Where things stand, as a sentence: a badge says less, and an address like "/spend-watch" says nothing.
|
|
383
419
|
const status = saveState === "saving"
|
|
384
420
|
? "Saving…"
|
|
@@ -391,7 +427,24 @@ export function HornStudio(props) {
|
|
|
391
427
|
: "Not saved yet";
|
|
392
428
|
const showChat = !mobile || pane === "chat";
|
|
393
429
|
const showPreview = !mobile || pane === "preview";
|
|
394
|
-
|
|
430
|
+
const reading = (() => {
|
|
431
|
+
const [first] = toRead;
|
|
432
|
+
if (first === undefined)
|
|
433
|
+
return undefined;
|
|
434
|
+
const view = latest.horn.views.find((candidate) => candidate.id === first);
|
|
435
|
+
if (view === undefined || "ref" in view || view.render.kind !== "tsx")
|
|
436
|
+
return undefined;
|
|
437
|
+
return { id: first, title: view.title, source: view.render.source, state: reviewStates[first] ?? "unread" };
|
|
438
|
+
})();
|
|
439
|
+
return (_jsxs("div", { "data-horn-studio": true, className: "flex h-svh w-full flex-col bg-sidebar", children: [reading === undefined ? null : (_jsx(ReadTheCodeDialog, { viewId: reading.id, title: reading.title, source: reading.source, state: reading.state, open: true, standing: { at: readingTotal - toRead.length + 1, of: readingTotal }, onClose: () => {
|
|
440
|
+
// "Not yet" stops the whole save, rather than moving on to the next one unread.
|
|
441
|
+
setToRead([]);
|
|
442
|
+
setSavingAfterReading(false);
|
|
443
|
+
}, onKeep: (change) => {
|
|
444
|
+
// Only once the keep is in the draft: the save that follows the last one reads the draft, and
|
|
445
|
+
// a draft still missing it looks exactly like code nobody read.
|
|
446
|
+
void edit(change, `Read and kept the code for "${reading.title}".`).then(() => setToRead((was) => was.slice(1)));
|
|
447
|
+
} })), _jsxs("header", { className: "flex h-14 shrink-0 items-center gap-3 px-3", children: [mobile ? (_jsx(Button, { variant: "ghost", size: "sm", iconOnly: true, icon: ArrowLeft, "aria-label": "All Horns", onClick: () => navigate(backTo) })) : (_jsx(Button, { variant: "ghost", size: "sm", icon: ArrowLeft, showLeftIcon: true, onClick: () => navigate(backTo), children: "All Horns" })), _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate text-sm font-medium text-default-foreground", children: latest.horn.title }), _jsx("span", { "data-save-status": true, className: "truncate text-xs text-muted-foreground", children: status })] }), mobile ? (_jsx("div", { className: "flex items-center gap-1 rounded-full bg-muted p-1", role: "group", "aria-label": "Show", children: ["chat", "preview"].map((each) => (_jsx(Button, { variant: pane === each ? "secondary" : "ghost", size: "xs", "aria-pressed": pane === each, onClick: () => setPane(each), children: each === "chat" ? "Chat" : "Preview" }, each))) })) : null, _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [unsaved ? (_jsx(Button, { variant: "secondary", size: "sm", disabled: saveState === "saving" || pending, onClick: discard, children: mobile ? "Discard" : "Discard changes" })) : null, _jsx(Button, { variant: "primary", size: "sm", disabled: !unsaved || saveState === "saving" || pending, onClick: () => void save(), children: saveState === "saving" ? "Saving…" : mobile ? "Save" : "Save changes" })] })] }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [showChat ? (_jsxs("aside", { ref: panelRef, "aria-label": "Studio chat", className: "flex min-h-0 shrink-0 flex-col", style: mobile ? { width: "100%" } : { width: panelWidth }, children: [isPresent(saveFailure) ? (_jsx("div", { className: "px-4 pt-2", children: _jsx(ErrorState, { title: "That save did not go through", hint: saveFailure }) })) : null, _jsx(StudioChat, { entries: entries, pending: pending, progress: progress, previewing: shown.id, welcome: welcome ?? "What would you like to change?", reassurance: reassurance ?? "Say it in your own words. Nothing is saved until you choose Save changes.", starters: starters ?? restyleStarters, placeholder: placeholder ?? "Describe a change, in your own words…", onSend: (text) => void send(text), onPreview: (id) => {
|
|
395
448
|
setViewing(id === latest.id ? undefined : id);
|
|
396
449
|
if (mobile)
|
|
397
450
|
setPane("preview");
|
|
@@ -3,6 +3,7 @@ import { useState } from "react";
|
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
4
|
import { isPresent } from "@manta-eu/lib/present";
|
|
5
5
|
import { Link, useParams, useSearchParams } from "react-router";
|
|
6
|
+
import { useActiveDatastore } from "@manta-eu/react/providers";
|
|
6
7
|
import { Camera, Link2, Pencil } from "lucide-react";
|
|
7
8
|
import { Button, Input } from "@manta-eu/react";
|
|
8
9
|
import { hornSnapshotSchema, variablesFromSearch } from "@manta-eu/horns/render";
|
|
@@ -11,24 +12,23 @@ import { writtenHornView } from "../components/written-horn-view";
|
|
|
11
12
|
import { catalogCardView } from "../components/catalog-card-view";
|
|
12
13
|
import { PlanRunDialog, usePlanRuns } from "../components/plan-runs";
|
|
13
14
|
import { sendHornScreenshot } from "../components/rasterize";
|
|
15
|
+
import { useExplainDialog } from "../components/explain-dialog";
|
|
16
|
+
import { useHornVariables } from "../lib/horn-variables";
|
|
14
17
|
import { DatastoreMissing, ErrorState, Loading } from "../components/states";
|
|
15
18
|
import { hornPath, hornsPath } from "./paths";
|
|
16
19
|
import { useSavedHorn } from "./saved-horn";
|
|
17
20
|
import { VariableBar } from "./variable-bar";
|
|
18
21
|
import { isRecord } from "@manta-eu/horns";
|
|
19
|
-
import { MantaSparqlProvider } from "../lib/manta/providers";
|
|
20
|
-
import { useMantaSession } from "../lib/manta/session";
|
|
21
22
|
import { appTheme } from "../lib/theme";
|
|
22
23
|
// A saved Horn as the thing itself: what a shared link opens. No chat, nothing to save — the URL is the state,
|
|
23
24
|
// so the link somebody is sent draws exactly the Horn they were looking at, filters and all. The plans behind a
|
|
24
25
|
// view are still offered here, because a person who can open the Horn is a person who can act on it.
|
|
25
26
|
export function HornViewPage() {
|
|
26
|
-
const { datastoreId
|
|
27
|
-
const {
|
|
28
|
-
|
|
29
|
-
if (!isPresent(datastore))
|
|
27
|
+
const { datastoreId } = useActiveDatastore();
|
|
28
|
+
const { slug } = useParams();
|
|
29
|
+
if (!isPresent(datastoreId))
|
|
30
30
|
return _jsx(DatastoreMissing, {});
|
|
31
|
-
return
|
|
31
|
+
return _jsx(Viewer, { datastoreId: datastoreId, slug: slug ?? "" });
|
|
32
32
|
}
|
|
33
33
|
function ShareLink() {
|
|
34
34
|
const [shown, setShown] = useState();
|
|
@@ -60,10 +60,14 @@ function UpdatePicture({ datastoreId, slug, snapshot }) {
|
|
|
60
60
|
return (_jsxs(_Fragment, { children: [problem === undefined ? null : (_jsx("span", { role: "alert", className: "text-xs text-muted-foreground", children: problem })), _jsxs(Button, { variant: "secondary", size: "sm", disabled: state === "working", onClick: () => void take(), children: [_jsx(Camera, { size: 14, "aria-hidden": "true" }), state === "working" ? "Taking…" : state === "kept" ? "Picture updated" : "Update picture"] })] }));
|
|
61
61
|
}
|
|
62
62
|
function Viewer({ datastoreId, slug }) {
|
|
63
|
+
const explain = useExplainDialog();
|
|
63
64
|
const [search] = useSearchParams();
|
|
64
65
|
const saved = useSavedHorn(datastoreId, slug);
|
|
65
66
|
const horn = saved.data?.horn;
|
|
66
67
|
const variables = horn ? variablesFromSearch(horn, search.toString()) : {};
|
|
68
|
+
// Writing goes through nuqs, which owns the URL; reading stays with `variablesFromSearch`, which is what
|
|
69
|
+
// types a value against the variable that declared it.
|
|
70
|
+
const urlVariables = useHornVariables(horn);
|
|
67
71
|
const snapshot = useQuery({
|
|
68
72
|
enabled: isPresent(horn),
|
|
69
73
|
queryKey: ["horn-view", datastoreId, slug, saved.data?.revision ?? "", variables],
|
|
@@ -90,7 +94,7 @@ function Viewer({ datastoreId, slug }) {
|
|
|
90
94
|
}
|
|
91
95
|
if (!isPresent(horn))
|
|
92
96
|
return _jsx(Loading, { label: "Opening the Horn\u2026", rows: 3 });
|
|
93
|
-
return (_jsxs("div", { "data-horn-viewer": true, className: "flex w-full flex-col gap-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx(Link, { to: hornsPath(
|
|
97
|
+
return (_jsxs("div", { "data-horn-viewer": true, className: "flex w-full flex-col gap-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx(Link, { to: hornsPath(), className: "text-sm text-muted-foreground hover:text-default-foreground", children: "All Horns" }), _jsx(VariableBar, { horn: horn })] }), _jsxs("div", { className: "flex items-center gap-2", children: [planRuns.problem === undefined ? null : (_jsx("span", { role: "alert", className: "text-xs text-muted-foreground", children: planRuns.problem })), _jsx(UpdatePicture, { datastoreId: datastoreId, slug: slug, snapshot: snapshot.data }), _jsx(ShareLink, {}), _jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: _jsxs(Link, { to: hornPath(slug), children: [_jsx(Pencil, { size: 14, "aria-hidden": "true" }), "Change it"] }) })] })] }), snapshot.isError ? (_jsx(ErrorState, { title: "The Horn could not be drawn", hint: snapshot.error.message })) : !isPresent(snapshot.data) ? (_jsx(Loading, { label: "Drawing the Horn\u2026", rows: 4 })) : (
|
|
94
98
|
// The Horn draws its own page, title and canvas.
|
|
95
|
-
_jsxs("div", { "data-horn-preview": true, className: "w-full overflow-hidden rounded-xl", children: [_jsx(HornSnapshotView, { snapshot: snapshot.data, mode: "page", theme: appTheme(), planActions: planRuns.actions, writtenView: writtenHornView, catalogCard: catalogCardView }), _jsx(PlanRunDialog, { runs: planRuns, labels: snapshot.data.labels })] }))] }));
|
|
99
|
+
_jsxs("div", { "data-horn-preview": true, className: "w-full overflow-hidden rounded-xl", children: [_jsx(HornSnapshotView, { snapshot: snapshot.data, mode: "page", theme: appTheme(), planActions: planRuns.actions, writtenView: writtenHornView, catalogCard: catalogCardView, explain: explain.open, onVariableChange: urlVariables.set }), _jsx(PlanRunDialog, { runs: planRuns, labels: snapshot.data.labels }), explain.dialog] }))] }));
|
|
96
100
|
}
|
package/dist/app/horns-page.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { isPresent } from "@manta-eu/lib/present";
|
|
3
|
-
import { Link
|
|
3
|
+
import { Link } from "react-router";
|
|
4
|
+
import { useActiveDatastore } from "@manta-eu/react/providers";
|
|
4
5
|
import { ChevronRight, Columns3, Plus } from "lucide-react";
|
|
5
6
|
import { Button } from "@manta-eu/react";
|
|
6
7
|
import { PageHeader } from "../components/page-header";
|
|
@@ -8,15 +9,11 @@ import { DatastoreMissing, EmptyState, ErrorState, Loading } from "../components
|
|
|
8
9
|
import { hornViewPath, newHornPath } from "./paths";
|
|
9
10
|
import { useQuery } from "@tanstack/react-query";
|
|
10
11
|
import { isRecord } from "@manta-eu/horns";
|
|
11
|
-
import { MantaSparqlProvider } from "../lib/manta/providers";
|
|
12
|
-
import { useMantaSession } from "../lib/manta/session";
|
|
13
12
|
export function HornsPage() {
|
|
14
|
-
const { datastoreId } =
|
|
15
|
-
|
|
16
|
-
const datastore = datastores.find((candidate) => candidate.id === datastoreId);
|
|
17
|
-
if (!isPresent(datastore))
|
|
13
|
+
const { datastoreId } = useActiveDatastore();
|
|
14
|
+
if (!isPresent(datastoreId))
|
|
18
15
|
return _jsx(DatastoreMissing, {});
|
|
19
|
-
return
|
|
16
|
+
return _jsx(HornsIn, { datastoreId: datastoreId });
|
|
20
17
|
}
|
|
21
18
|
function HornsIn({ datastoreId }) {
|
|
22
19
|
const horns = useQuery({
|
|
@@ -39,5 +36,5 @@ function HornsIn({ datastoreId }) {
|
|
|
39
36
|
: []);
|
|
40
37
|
},
|
|
41
38
|
});
|
|
42
|
-
return (_jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: "Horns", description: "Dashboards and apps made from your data. Open one to look at it or change it, or describe a new one.", actions: _jsx(Button, { variant: "primary", asChild: true, children: _jsxs(Link, { to: newHornPath(
|
|
39
|
+
return (_jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: "Horns", description: "Dashboards and apps made from your data. Open one to look at it or change it, or describe a new one.", actions: _jsx(Button, { variant: "primary", asChild: true, children: _jsxs(Link, { to: newHornPath(), children: [_jsx(Plus, { size: 16, "aria-hidden": "true" }), "New Horn"] }) }) }), horns.isPending ? (_jsx(Loading, { label: "Reading this datastore's Horns\u2026" })) : horns.isError ? (_jsx(ErrorState, { title: "Your Horns could not be loaded", hint: "Try again in a moment. If it keeps happening, the data service may be down." })) : !isPresent(horns.data) ? (_jsx(EmptyState, { title: "No Horns yet", hint: "Describe what you want to see, in your own words, and it is built for you from this datastore's data.", icon: _jsx(Columns3, { size: 20 }), action: _jsx(Button, { variant: "primary", size: "sm", asChild: true, children: _jsxs(Link, { to: newHornPath(), children: [_jsx(Plus, { size: 14, "aria-hidden": "true" }), "Make your first Horn"] }) }) })) : (_jsx("ul", { "data-horns": true, className: "grid gap-3 sm:grid-cols-2 md:grid-cols-3", children: horns.data.map((horn) => (_jsx("li", { children: _jsxs(Link, { to: hornViewPath(horn.slug), className: "flex items-center gap-3 rounded-lg border border-default-border bg-card px-4 py-3 transition-colors hover:bg-card/70", children: [_jsx("span", { className: "grid size-9 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground", children: _jsx(Columns3, { size: 16 }) }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate text-sm font-medium text-default-foreground", children: horn.title }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: horn.number === 1 ? "Saved once" : `Saved ${horn.number} times` })] }), _jsx(ChevronRight, { size: 16, "aria-hidden": "true", className: "shrink-0 text-muted-foreground" })] }) }, horn.slug))) }))] }));
|
|
43
40
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useSearchParams } from "react-router";
|
|
5
|
+
import { z } from "zod";
|
|
3
6
|
import { isPresent } from "@manta-eu/lib/present";
|
|
4
|
-
import {
|
|
7
|
+
import { useActiveDatastore } from "@manta-eu/react/providers";
|
|
8
|
+
import { hornSchema } from "@manta-eu/horns";
|
|
5
9
|
import { standaloneHorn } from "@manta-eu/horns/shapes";
|
|
6
|
-
import { DatastoreMissing } from "../components/states";
|
|
10
|
+
import { DatastoreMissing, ErrorState, Loading } from "../components/states";
|
|
7
11
|
import { hornsPath } from "./paths";
|
|
8
12
|
import { HornStudio } from "./horn-studio";
|
|
9
|
-
import {
|
|
10
|
-
import { useMantaSession } from "../lib/manta/session";
|
|
13
|
+
import { problemFrom } from "../lib/problem";
|
|
11
14
|
import { shapesRealm } from "../lib/shapes/catalog";
|
|
12
15
|
// The way in: a Horn from nothing. It opens straight into the Studio with an empty document, the way a new
|
|
13
16
|
// Lovable project opens on its composer: the first thing typed is what it should show, the model names it as
|
|
@@ -18,17 +21,56 @@ const starterPrompts = [
|
|
|
18
21
|
"What happened in search last week that I should look at?",
|
|
19
22
|
];
|
|
20
23
|
export function NewHornPage() {
|
|
21
|
-
const { datastoreId } =
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
if (!isPresent(
|
|
24
|
+
const { datastoreId } = useActiveDatastore();
|
|
25
|
+
const [search] = useSearchParams();
|
|
26
|
+
const about = search.get("about");
|
|
27
|
+
if (!isPresent(datastoreId)) {
|
|
25
28
|
return (_jsx("main", { className: "flex h-svh w-full items-center justify-center bg-sidebar p-6", children: _jsx(DatastoreMissing, {}) }));
|
|
26
29
|
}
|
|
27
|
-
return (_jsx(
|
|
30
|
+
return isPresent(about) ? (_jsx(NewHornAbout, { datastoreId: datastoreId, about: about })) : (_jsx(NewHorn, { datastoreId: datastoreId }));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* What the lookup answers with: the Horn, and whether it is one somebody kept. A kept Horn brings its slug
|
|
34
|
+
* and the revision it is at, which is what saving an edit of it needs — without them a save would write a
|
|
35
|
+
* second Horn about the same thing, or be refused for revising a Horn it never read.
|
|
36
|
+
*/
|
|
37
|
+
const aboutAnswer = z.object({
|
|
38
|
+
horn: hornSchema,
|
|
39
|
+
kept: z.boolean(),
|
|
40
|
+
slug: z.string().min(1).optional(),
|
|
41
|
+
revision: z.string().min(1).optional(),
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* A Horn started from the thing it is about, rather than from nothing.
|
|
45
|
+
*
|
|
46
|
+
* The worker already builds one for a rule or a plan that nobody has kept — that is what the route answers
|
|
47
|
+
* with — so this opens the Studio on it. Still a draft: it becomes the Horn for that thing when somebody
|
|
48
|
+
* saves it, and `about` is what makes the next lookup find it. One somebody already kept opens as itself,
|
|
49
|
+
* so that saving revises it rather than making another.
|
|
50
|
+
*/
|
|
51
|
+
function NewHornAbout({ datastoreId, about }) {
|
|
52
|
+
const seed = useQuery({
|
|
53
|
+
queryKey: ["horn-about", datastoreId, about],
|
|
54
|
+
queryFn: async ({ signal }) => {
|
|
55
|
+
const at = `/api/d/${encodeURIComponent(datastoreId)}/horns/by/about?iri=${encodeURIComponent(about)}`;
|
|
56
|
+
const response = await fetch(at, { signal });
|
|
57
|
+
if (!response.ok)
|
|
58
|
+
throw new Error(await problemFrom(response, `The answer was ${response.status}.`));
|
|
59
|
+
return aboutAnswer.parse(await response.json());
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
if (seed.isPending) {
|
|
63
|
+
return (_jsx("main", { className: "flex h-svh w-full items-center justify-center bg-sidebar p-6", children: _jsx(Loading, { label: "Building a Horn for this\u2026" }) }));
|
|
64
|
+
}
|
|
65
|
+
if (seed.isError) {
|
|
66
|
+
return (_jsx("main", { className: "flex h-svh w-full items-center justify-center bg-sidebar p-6", children: _jsx(ErrorState, { title: "Nothing could be built for this", hint: seed.error.message }) }));
|
|
67
|
+
}
|
|
68
|
+
const { horn, kept, slug, revision } = seed.data;
|
|
69
|
+
return (_jsx(HornStudio, { datastoreId: datastoreId, stored: horn, ...(isPresent(slug) ? { slug } : {}), ...(isPresent(revision) ? { revision } : {}), backTo: hornsPath(), welcome: "Here is what this shows. What would you change?", reassurance: kept ? "Saving keeps your changes to this Horn." : "Nothing is saved until you save it.", placeholder: "Describe what you want to see\u2026" }));
|
|
28
70
|
}
|
|
29
71
|
function NewHorn({ datastoreId }) {
|
|
30
72
|
// Made once: the route owns the IRI and the revision on save, and this is a document that has never been
|
|
31
73
|
// stored. A new render must not make a new one underneath the draft.
|
|
32
74
|
const [horn] = useState(() => standaloneHorn({ id: "untitled", title: "Untitled Horn", views: [], realm: shapesRealm(), datastoreId }));
|
|
33
|
-
return (_jsx(HornStudio, { datastoreId: datastoreId, stored: horn, backTo: hornsPath(
|
|
75
|
+
return (_jsx(HornStudio, { datastoreId: datastoreId, stored: horn, backTo: hornsPath(), welcome: "What should this Horn show?", reassurance: "Say it in your own words, and I will build it from this datastore's data.", placeholder: "Describe what you want to see\u2026", starters: starterPrompts }));
|
|
34
76
|
}
|
package/dist/app/paths.d.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
export declare
|
|
2
|
-
export declare
|
|
3
|
-
|
|
1
|
+
export declare const hornsPath: () => string;
|
|
2
|
+
export declare const newHornPath: () => string;
|
|
3
|
+
/** A new Horn started from the thing it is about: a rule, an action plan. The IRI rides as a parameter. */
|
|
4
|
+
export declare const newHornAboutPath: (iri: string) => string;
|
|
5
|
+
export declare const hornPath: (slug: string) => string;
|
|
4
6
|
/** A saved Horn as the thing itself: what a shared link opens, with the variables it carries. */
|
|
5
|
-
export declare
|
|
6
|
-
export declare function shapesPath(datastoreId: string): string;
|
|
7
|
-
export declare function shapeZooPath(datastoreId: string, schema: string): string;
|
|
8
|
-
export declare function shapeEntityPath(datastoreId: string, schema: string, entity: string): string;
|
|
7
|
+
export declare const hornViewPath: (slug: string) => string;
|
|
9
8
|
/** A datastore's Horns as cards: what each one answers, who it is for, what it reads. */
|
|
10
|
-
export declare const showcasePath: (
|
|
9
|
+
export declare const showcasePath: () => string;
|
|
11
10
|
/** What Manta works out about a datastore: every rule, and whether it can be looked at. */
|
|
12
|
-
export declare
|
|
13
|
-
export declare
|
|
11
|
+
export declare const rulesPath: () => string;
|
|
12
|
+
export declare const plansPath: () => string;
|
|
13
|
+
export declare const rulePath: (ruleId: string) => string;
|
|
14
|
+
/** The reference Horn drawn against a fixture. Dev only; see docs/standard.md. */
|
|
15
|
+
export declare const referencePath: () => string;
|
|
16
|
+
export declare const shapesPath: () => string;
|
|
17
|
+
export declare const shapeZooPath: (schema: string) => string;
|
|
18
|
+
export declare const shapeEntityPath: (schema: string, entity: string) => string;
|
package/dist/app/paths.js
CHANGED
|
@@ -1,32 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
export
|
|
8
|
-
return `${hornsPath(datastoreId)}/${encodeURIComponent(slug)}`;
|
|
9
|
-
}
|
|
1
|
+
// Relative to the datastore: `ActiveDatastoreProvider` scopes the router's basename to `/d/<datastoreId>`,
|
|
2
|
+
// so a helper that wrote the prefix itself would double it.
|
|
3
|
+
export const hornsPath = () => "/horns";
|
|
4
|
+
export const newHornPath = () => "/horns/new";
|
|
5
|
+
/** A new Horn started from the thing it is about: a rule, an action plan. The IRI rides as a parameter. */
|
|
6
|
+
export const newHornAboutPath = (iri) => `/horns/new?about=${encodeURIComponent(iri)}`;
|
|
7
|
+
export const hornPath = (slug) => `/horns/${encodeURIComponent(slug)}`;
|
|
10
8
|
/** A saved Horn as the thing itself: what a shared link opens, with the variables it carries. */
|
|
11
|
-
export
|
|
12
|
-
return `${hornPath(datastoreId, slug)}/view`;
|
|
13
|
-
}
|
|
14
|
-
export function shapesPath(datastoreId) {
|
|
15
|
-
return `/d/${encodeURIComponent(datastoreId)}/shapes`;
|
|
16
|
-
}
|
|
17
|
-
export function shapeZooPath(datastoreId, schema) {
|
|
18
|
-
return `${shapesPath(datastoreId)}/${encodeURIComponent(schema)}`;
|
|
19
|
-
}
|
|
20
|
-
// An entity IRI carries slashes and colons, so the entity travels as a query parameter rather than a segment.
|
|
21
|
-
export function shapeEntityPath(datastoreId, schema, entity) {
|
|
22
|
-
return `${shapeZooPath(datastoreId, schema)}/entity?iri=${encodeURIComponent(entity)}`;
|
|
23
|
-
}
|
|
9
|
+
export const hornViewPath = (slug) => `${hornPath(slug)}/view`;
|
|
24
10
|
/** A datastore's Horns as cards: what each one answers, who it is for, what it reads. */
|
|
25
|
-
export const showcasePath = (
|
|
11
|
+
export const showcasePath = () => "/horns/showcase";
|
|
26
12
|
/** What Manta works out about a datastore: every rule, and whether it can be looked at. */
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
13
|
+
export const rulesPath = () => "/horns/rules";
|
|
14
|
+
export const plansPath = () => "/horns/plans";
|
|
15
|
+
export const rulePath = (ruleId) => `${rulesPath()}/${encodeURIComponent(ruleId)}`;
|
|
16
|
+
/** The reference Horn drawn against a fixture. Dev only; see docs/standard.md. */
|
|
17
|
+
export const referencePath = () => "/horns/reference";
|
|
18
|
+
export const shapesPath = () => "/horns/shapes";
|
|
19
|
+
export const shapeZooPath = (schema) => `${shapesPath()}/${encodeURIComponent(schema)}`;
|
|
20
|
+
// An entity IRI carries slashes and colons, so the entity travels as a query parameter rather than a segment.
|
|
21
|
+
export const shapeEntityPath = (schema, entity) => `${shapeZooPath(schema)}/entity?iri=${encodeURIComponent(entity)}`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function PlansPage(): import("react").JSX.Element;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { isPresent } from "@manta-eu/lib/present";
|
|
4
|
+
import { Link } from "react-router";
|
|
5
|
+
import { useActiveDatastore } from "@manta-eu/react/providers";
|
|
6
|
+
import { ChevronRight } from "lucide-react";
|
|
7
|
+
import { Badge, Skeleton } from "@manta-eu/react";
|
|
8
|
+
import { planNameFromIri, plansQuery, readPlans, planStatusQuery, readPlanStatus, } from "@manta-eu/horns/render";
|
|
9
|
+
import { newHornAboutPath } from "./paths";
|
|
10
|
+
import { Card, CardTitle, PageHeader } from "../components/page-header";
|
|
11
|
+
import { DatastoreMissing, EmptyState, ErrorState } from "../components/states";
|
|
12
|
+
import { useSparqlSelect } from "../lib/manta/sparql-select";
|
|
13
|
+
import { shapesRealm } from "../lib/shapes/catalog";
|
|
14
|
+
// The work Manta has drafted, and the way to make a Horn out of one. A plan is a named graph, so it has no
|
|
15
|
+
// page of its own here: the Horn is the page, and this is where you start one.
|
|
16
|
+
const stateWords = {
|
|
17
|
+
draft: { text: "Not run yet", variant: "info" },
|
|
18
|
+
running: { text: "Running", variant: "info" },
|
|
19
|
+
completed: { text: "Done", variant: "success" },
|
|
20
|
+
failed: { text: "Did not go through", variant: "warning" },
|
|
21
|
+
"timed-out": { text: "Took too long", variant: "warning" },
|
|
22
|
+
};
|
|
23
|
+
// Not the SDK's `PlanRow` (`@manta-eu/react/plan-row`): its `PlanRowPlan` wants `actionCount`,
|
|
24
|
+
// `completedActionCount`, `subjects` and a `GQLIntegration`, which come from agents' GraphQL — and the
|
|
25
|
+
// browser gets no GraphQL route (AGENTS.md §Writes). This list reads the plans graph over SPARQL, so it
|
|
26
|
+
// holds the IRI, the name and the state and nothing that would fill those fields honestly.
|
|
27
|
+
function ListedPlan({ plan }) {
|
|
28
|
+
const said = stateWords[plan.status.state];
|
|
29
|
+
return (_jsxs("li", { "data-plan": plan.iri, className: "flex flex-wrap items-center justify-between gap-3 px-4 py-2.5", children: [_jsx("span", { className: "flex min-w-0 flex-col", children: _jsx("span", { className: "text-default-foreground truncate text-sm font-medium", children: plan.status.name ?? planNameFromIri(plan.iri) ?? "Untitled plan" }) }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx(Badge, { variant: said.variant, children: said.text }), _jsxs(Link, { className: "text-muted-foreground hover:text-default-foreground flex items-center gap-1 text-sm", to: newHornAboutPath(plan.iri), children: ["Make a Horn from this", _jsx(ChevronRight, { size: 14, "aria-hidden": "true" })] })] })] }));
|
|
30
|
+
}
|
|
31
|
+
function Plans({ datastoreId }) {
|
|
32
|
+
const select = useSparqlSelect();
|
|
33
|
+
const realm = shapesRealm();
|
|
34
|
+
const listed = useQuery({
|
|
35
|
+
queryKey: ["plans", datastoreId],
|
|
36
|
+
queryFn: async ({ signal }) => {
|
|
37
|
+
const iris = readPlans(await select(plansQuery(realm), signal));
|
|
38
|
+
if (iris.length === 0)
|
|
39
|
+
return [];
|
|
40
|
+
// Names and states come from the plans graph, not from the IRIs, which say nothing.
|
|
41
|
+
const statuses = readPlanStatus(await select(planStatusQuery(realm, iris), signal));
|
|
42
|
+
return iris.map((iri) => ({ iri, status: statuses.get(iri) ?? { state: "draft" } }));
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
if (listed.isPending)
|
|
46
|
+
return _jsx(Skeleton, { className: "h-64 w-full" });
|
|
47
|
+
if (listed.isError)
|
|
48
|
+
return _jsx(ErrorState, { title: "The plans could not be read.", hint: listed.error.message });
|
|
49
|
+
if (!isPresent(listed.data)) {
|
|
50
|
+
return (_jsx(EmptyState, { title: "No action plans yet", hint: "Manta drafts these when a rule finds work to do. When one appears, you can make a Horn from it here." }));
|
|
51
|
+
}
|
|
52
|
+
return (_jsxs(Card, { children: [_jsx(CardTitle, { children: "What Manta has drafted" }), _jsx("ul", { className: "divide-default-border divide-y", children: listed.data.map((plan) => (_jsx(ListedPlan, { plan: plan }, plan.iri))) })] }));
|
|
53
|
+
}
|
|
54
|
+
export function PlansPage() {
|
|
55
|
+
const { datastoreId } = useActiveDatastore();
|
|
56
|
+
if (!isPresent(datastoreId))
|
|
57
|
+
return _jsx(DatastoreMissing, {});
|
|
58
|
+
return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(PageHeader, { title: "Action plans", description: "The changes Manta has worked out and not made. Open one as a Horn to see what it would do." }), _jsx(Plans, { datastoreId: datastoreId })] }));
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ReferencePage(): import("react").JSX.Element;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { hornSchema } from "@manta-eu/horns";
|
|
5
|
+
import { buildHornSnapshot } from "@manta-eu/horns/render";
|
|
6
|
+
import { HornSnapshotView } from "@manta-eu/horns-kit";
|
|
7
|
+
import { writtenHornView } from "../components/written-horn-view";
|
|
8
|
+
import { catalogCardView } from "../components/catalog-card-view";
|
|
9
|
+
import { useExplainDialog } from "../components/explain-dialog";
|
|
10
|
+
import { ErrorState, Loading } from "../components/states";
|
|
11
|
+
import { PageHeader } from "../components/page-header";
|
|
12
|
+
import { useHornVariables } from "../lib/horn-variables";
|
|
13
|
+
import { fakeSparqlSelect, referenceEntity } from "../lib/reference/fake-sparql";
|
|
14
|
+
import { appTheme } from "../lib/theme";
|
|
15
|
+
// The reference Horn from docs/standard.md, drawn against a datastore that does not exist. Dev only, and the
|
|
16
|
+
// document and its fixture are imported lazily so neither reaches a deployed bundle.
|
|
17
|
+
//
|
|
18
|
+
// It is the whole real path — the binding compiles, the label pass runs, the written source is transpiled and
|
|
19
|
+
// bound to the design system — with `select` swapped for a stub. That is what makes it worth looking at: what
|
|
20
|
+
// is on screen is what a datastore holding this data would show.
|
|
21
|
+
/** Parsed once: the document is a static import and re-parsing it per render would rebuild every view. */
|
|
22
|
+
function useReferenceHorn() {
|
|
23
|
+
return useMemo(async () => {
|
|
24
|
+
const document = (await import("../reference/supply-chain-360.horn.json")).default;
|
|
25
|
+
return hornSchema.parse(document);
|
|
26
|
+
}, []);
|
|
27
|
+
}
|
|
28
|
+
export function ReferencePage() {
|
|
29
|
+
const explain = useExplainDialog();
|
|
30
|
+
const pending = useReferenceHorn();
|
|
31
|
+
const loaded = useQuery({ queryKey: ["reference-horn-document"], queryFn: () => pending });
|
|
32
|
+
// What the page is narrowed to, in the URL. A view sets one and every binding taking it as a parameter is
|
|
33
|
+
// asked again, which is the whole of how one card filters another.
|
|
34
|
+
const filters = useHornVariables(loaded.data);
|
|
35
|
+
const drawn = useQuery({
|
|
36
|
+
enabled: loaded.data !== undefined,
|
|
37
|
+
// The page it drew stays up while the next one is asked for, so narrowing does not blink.
|
|
38
|
+
placeholderData: keepPreviousData,
|
|
39
|
+
queryKey: ["reference-horn", filters.values],
|
|
40
|
+
queryFn: async ({ signal }) => {
|
|
41
|
+
const horn = loaded.data;
|
|
42
|
+
if (horn === undefined)
|
|
43
|
+
throw new Error("The reference Horn has not loaded.");
|
|
44
|
+
return await buildHornSnapshot({
|
|
45
|
+
horn,
|
|
46
|
+
select: fakeSparqlSelect,
|
|
47
|
+
variables: { entity: referenceEntity, ...filters.values },
|
|
48
|
+
// Plans come from a datastore, and there is none.
|
|
49
|
+
plans: false,
|
|
50
|
+
signal,
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(PageHeader, { title: "Supply Chain 360", description: "The reference Horn from docs/standard.md, drawn against a fixture. No datastore is read." }), drawn.isPending && drawn.data === undefined ? (_jsx(Loading, { label: "Drawing the reference Horn\u2026", rows: 4 })) : drawn.isError ? (_jsx(ErrorState, { title: "The reference Horn could not be drawn", hint: drawn.error.message })) : (_jsxs("div", { "data-horn-preview": true, className: "w-full overflow-hidden rounded-xl", children: [_jsx(HornSnapshotView, { snapshot: drawn.data, mode: "page", theme: appTheme(), writtenView: writtenHornView, catalogCard: catalogCardView, explain: explain.open, onVariableChange: filters.set }), explain.dialog] }))] }));
|
|
55
|
+
}
|