@manta-eu/horns-client 0.1.0 → 0.1.1

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.
Files changed (71) hide show
  1. package/component-api.d.ts +7 -0
  2. package/dist/app/coverage-run.js +4 -3
  3. package/dist/app/home-page.js +6 -4
  4. package/dist/app/horn-page.js +7 -10
  5. package/dist/app/horn-studio.js +63 -10
  6. package/dist/app/horn-view-page.js +13 -9
  7. package/dist/app/horns-page.js +6 -9
  8. package/dist/app/new-horn-page.js +52 -10
  9. package/dist/app/paths.d.ts +15 -10
  10. package/dist/app/paths.js +18 -29
  11. package/dist/app/plans-page.d.ts +1 -0
  12. package/dist/app/plans-page.js +59 -0
  13. package/dist/app/reference-page.d.ts +1 -0
  14. package/dist/app/reference-page.js +55 -0
  15. package/dist/app/routes.js +7 -5
  16. package/dist/app/rule-page.js +18 -11
  17. package/dist/app/rules-page.js +12 -12
  18. package/dist/app/shape-entity-page.js +7 -9
  19. package/dist/app/shape-zoo-page.js +9 -10
  20. package/dist/app/shapes-page.js +13 -15
  21. package/dist/app/showcase-page.js +8 -11
  22. package/dist/components/app-shell.js +8 -7
  23. package/dist/components/catalog-card-view.js +4 -5
  24. package/dist/components/explain-dialog.d.ts +5 -0
  25. package/dist/components/explain-dialog.js +26 -0
  26. package/dist/components/plan-runs.js +2 -2
  27. package/dist/components/read-the-code.d.ts +19 -0
  28. package/dist/components/read-the-code.js +17 -3
  29. package/dist/index.d.ts +2 -3
  30. package/dist/index.js +2 -3
  31. package/dist/lib/horn-review.d.ts +7 -0
  32. package/dist/lib/horn-review.js +15 -0
  33. package/dist/lib/horn-variables.d.ts +8 -0
  34. package/dist/lib/horn-variables.js +39 -0
  35. package/dist/lib/manta/sign-in.d.ts +2 -0
  36. package/dist/lib/manta/sign-in.js +9 -0
  37. package/dist/lib/manta/sparql-select.d.ts +6 -2
  38. package/dist/lib/manta/sparql-select.js +17 -14
  39. package/dist/lib/reference/fake-sparql.d.ts +4 -0
  40. package/dist/lib/reference/fake-sparql.js +216 -0
  41. package/dist/lib/shapes/ldkit-source.d.ts +1 -1
  42. package/dist/lib/shapes/ldkit-source.js +1 -1
  43. package/dist/lib/studio-progress.js +3 -1
  44. package/dist/lib/surface/declarations.d.ts +5 -0
  45. package/dist/lib/surface/declarations.js +77 -0
  46. package/dist/lib/written-surface.js +13 -2
  47. package/dist/standalone/app.d.ts +2 -0
  48. package/dist/standalone/app.js +57 -0
  49. package/dist/standalone/client.d.ts +15 -0
  50. package/dist/{client.js → standalone/client.js} +12 -8
  51. package/dist/standalone/gates.d.ts +10 -0
  52. package/dist/standalone/gates.js +15 -0
  53. package/dist/styles.css +1 -1
  54. package/dist/vite/component-api.d.ts +8 -0
  55. package/dist/vite/component-api.js +24 -0
  56. package/dist/vite/index.d.ts +3 -0
  57. package/dist/vite/index.js +3 -0
  58. package/dist/vite/shape-catalog.d.ts +10 -0
  59. package/dist/vite/shape-catalog.js +35 -0
  60. package/dist/vite/typecheck-fs.d.ts +6 -0
  61. package/dist/vite/typecheck-fs.js +68 -0
  62. package/package.json +36 -12
  63. package/shape-catalog.d.ts +9 -0
  64. package/typecheck-fs.d.ts +6 -0
  65. package/dist/app/session-gate.d.ts +0 -6
  66. package/dist/app/session-gate.js +0 -19
  67. package/dist/client.d.ts +0 -10
  68. package/dist/lib/manta/providers.d.ts +0 -10
  69. package/dist/lib/manta/providers.js +0 -48
  70. package/dist/lib/manta/session.d.ts +0 -47
  71. package/dist/lib/manta/session.js +0 -204
@@ -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("../../../../docs/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
+ }
@@ -1,21 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Navigate, Outlet, Route, Routes } from "react-router";
3
+ import { NuqsAdapter } from "nuqs/adapters/react-router/v7";
3
4
  import { AppShell } from "../components/app-shell";
4
5
  import { HomePage } from "./home-page";
5
6
  import { HornPage } from "./horn-page";
6
7
  import { HornsPage } from "./horns-page";
7
8
  import { HornViewPage } from "./horn-view-page";
8
9
  import { NewHornPage } from "./new-horn-page";
9
- import { SessionGate } from "./session-gate";
10
10
  import { RulePage } from "./rule-page";
11
+ import { PlansPage } from "./plans-page";
11
12
  import { RulesPage } from "./rules-page";
12
13
  import { ShapeEntityPage } from "./shape-entity-page";
13
14
  import { ShapeZooPage } from "./shape-zoo-page";
14
15
  import { ShapesPage } from "./shapes-page";
15
16
  import { ShowcasePage } from "./showcase-page";
16
- function GatedOutlet() {
17
- return (_jsx(SessionGate, { children: _jsx(Outlet, {}) }));
18
- }
17
+ import { ReferencePage } from "./reference-page";
19
18
  /**
20
19
  * Horns' routes as `<Route>` children, for a host that splices them into its own `<Routes>`. A nested
21
20
  * `<Routes>` would rebase every path on its parent's match, so a host mounting Horns under a prefix would
@@ -25,7 +24,10 @@ function GatedOutlet() {
25
24
  * of its own leaves it off and keeps its own 404.
26
25
  */
27
26
  export function hornsRoutes({ catchAll = false } = {}) {
28
- return (_jsxs(Route, { element: _jsx(GatedOutlet, {}), children: [_jsx(Route, { path: "d/:datastoreId/horns/new", element: _jsx(NewHornPage, {}) }), _jsx(Route, { path: "d/:datastoreId/horns/:slug", element: _jsx(HornPage, {}) }), _jsxs(Route, { element: _jsx(AppShell, {}), children: [_jsx(Route, { path: "d/:datastoreId/horns", element: _jsx(HornsPage, {}) }), _jsx(Route, { path: "d/:datastoreId/horns/:slug/view", element: _jsx(HornViewPage, {}) }), _jsx(Route, { path: "d/:datastoreId/showcase", element: _jsx(ShowcasePage, {}) }), _jsx(Route, { path: "d/:datastoreId/rules", element: _jsx(RulesPage, {}) }), _jsx(Route, { path: "d/:datastoreId/rules/:rule", element: _jsx(RulePage, {}) }), _jsx(Route, { path: "d/:datastoreId/shapes", element: _jsx(ShapesPage, {}) }), _jsx(Route, { path: "d/:datastoreId/shapes/:schema", element: _jsx(ShapeZooPage, {}) }), _jsx(Route, { path: "d/:datastoreId/shapes/:schema/entity", element: _jsx(ShapeEntityPage, {}) }), catchAll ? _jsx(Route, { index: true, element: _jsx(HomePage, {}) }) : null] }), catchAll ? _jsx(Route, { path: "*", element: _jsx(Navigate, { to: "/", replace: true }) }) : null] }));
27
+ return (
28
+ // Here rather than at the mount: a host splicing these into its own router gets it too, and a Horn page
29
+ // reads its variables through nuqs on every render — without an adapter that throws rather than degrades.
30
+ _jsxs(Route, { element: _jsx(NuqsAdapter, { children: _jsx(Outlet, {}) }), children: [_jsx(Route, { path: "horns/new", element: _jsx(NewHornPage, {}) }), _jsx(Route, { path: "horns/:slug", element: _jsx(HornPage, {}) }), _jsxs(Route, { element: _jsx(AppShell, {}), children: [_jsx(Route, { path: "horns", element: _jsx(HornsPage, {}) }), _jsx(Route, { path: "horns/:slug/view", element: _jsx(HornViewPage, {}) }), _jsx(Route, { path: "horns/showcase", element: _jsx(ShowcasePage, {}) }), _jsx(Route, { path: "horns/plans", element: _jsx(PlansPage, {}) }), _jsx(Route, { path: "horns/rules", element: _jsx(RulesPage, {}) }), _jsx(Route, { path: "horns/rules/:rule", element: _jsx(RulePage, {}) }), _jsx(Route, { path: "horns/shapes", element: _jsx(ShapesPage, {}) }), _jsx(Route, { path: "horns/shapes/:schema", element: _jsx(ShapeZooPage, {}) }), _jsx(Route, { path: "horns/shapes/:schema/entity", element: _jsx(ShapeEntityPage, {}) }), import.meta.env.DEV ? _jsx(Route, { path: "horns/reference", element: _jsx(ReferencePage, {}) }) : null, catchAll ? _jsx(Route, { index: true, element: _jsx(HomePage, {}) }) : null] }), catchAll ? _jsx(Route, { path: "*", element: _jsx(Navigate, { to: "/", replace: true }) }) : null] }));
29
31
  }
30
32
  export function AppRoutes() {
31
33
  return _jsx(Routes, { children: hornsRoutes({ catchAll: true }) });
@@ -1,24 +1,26 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useQuery } from "@tanstack/react-query";
3
3
  import { isPresent } from "@manta-eu/lib/present";
4
- import { useParams } from "react-router";
5
- import { Badge, Skeleton } from "@manta-eu/react";
4
+ import { Link, useParams } from "react-router";
5
+ import { useActiveDatastore } from "@manta-eu/react/providers";
6
+ import { Badge, Button, Skeleton } from "@manta-eu/react";
6
7
  import { buildHornSnapshot, maxSnapshotRows } from "@manta-eu/horns/render";
7
8
  import { standaloneHorn } from "@manta-eu/horns/shapes";
8
9
  import { rulesQuery, readRules } from "@manta-eu/horns/shapes";
9
10
  import { ruleCoverage, ruleHornView, ruleViewId } from "@manta-eu/horns/rules";
10
11
  import { Card, CardTitle, PageHeader } from "../components/page-header";
12
+ import { newHornAboutPath } from "./paths";
11
13
  import { DatastoreMissing, EmptyState, ErrorState } from "../components/states";
12
- import { MantaSparqlProvider } from "../lib/manta/providers";
13
- import { useMantaSession } from "../lib/manta/session";
14
- import { browserSparqlSelect } from "../lib/manta/sparql-select";
14
+ import { useSparqlSelect } from "../lib/manta/sparql-select";
15
15
  import { shapesRealm } from "../lib/shapes/catalog";
16
16
  import { SnapshotViewBody } from "../components/snapshot-view";
17
+ import { useAuthContext } from "@manta-eu/react/providers/AuthContext";
17
18
  // One rule, drawn. The view is built from the rule's own tree, so what is on screen is what the rule does —
18
19
  // and the Datalog sits under it, because a person judging the view has to be able to check it against the
19
20
  // rule rather than trust the parser.
20
21
  const ruleRowLimit = 60;
21
22
  function Drawn({ entry, datastoreId }) {
23
+ const select = useSparqlSelect();
22
24
  const realm = shapesRealm();
23
25
  const spec = entry.spec;
24
26
  const snapshot = useQuery({
@@ -36,7 +38,7 @@ function Drawn({ entry, datastoreId }) {
36
38
  realm,
37
39
  datastoreId,
38
40
  }),
39
- select: browserSparqlSelect(datastoreId),
41
+ select: select,
40
42
  plans: false,
41
43
  rowLimit: Math.min(ruleRowLimit, maxSnapshotRows),
42
44
  signal,
@@ -56,10 +58,11 @@ function Drawn({ entry, datastoreId }) {
56
58
  return _jsx(SnapshotViewBody, { snapshot: snapshot.data, view: view });
57
59
  }
58
60
  function Rule({ datastoreId, ruleId }) {
61
+ const select = useSparqlSelect();
59
62
  const realm = shapesRealm();
60
63
  const rules = useQuery({
61
64
  queryKey: ["rules", datastoreId],
62
- queryFn: async ({ signal }) => readRules(await browserSparqlSelect(datastoreId)(rulesQuery(realm, [], 200), signal)),
65
+ queryFn: async ({ signal }) => readRules(await select(rulesQuery(realm, [], 200), signal)),
63
66
  });
64
67
  if (rules.isPending)
65
68
  return _jsx(Skeleton, { className: "h-64 w-full" });
@@ -70,15 +73,19 @@ function Rule({ datastoreId, ruleId }) {
70
73
  return _jsx(EmptyState, { title: "This datastore runs no rule by that name." });
71
74
  return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(PageHeader, { title: entry.spec?.title ?? ruleId, description: entry.spec === undefined
72
75
  ? "Read, but it describes no view."
73
- : `What this rule works out, over the rows its own body selects.`, ...(entry.spec?.kind === "plan" ? { meta: _jsx(Badge, { variant: "info", children: "Drafts work" }) } : {}) }), _jsx(Drawn, { entry: entry, datastoreId: datastoreId }), _jsxs(Card, { children: [_jsx(CardTitle, { children: "The rule itself" }), _jsx("pre", { className: "text-muted-foreground overflow-auto px-4 pb-4 font-mono text-xs whitespace-pre-wrap", children: entry.rule.body })] })] }));
76
+ : `What this rule works out, over the rows its own body selects.`, ...(entry.spec?.kind === "plan" ? { meta: _jsx(Badge, { variant: "info", children: "Drafts work" }) } : {}), actions:
77
+ // The same Horn this page draws, opened in the Studio to change and keep. Unsaved until somebody
78
+ // saves it, and `about` is what makes it the Horn for this rule from then on.
79
+ entry.spec === undefined ? undefined : (_jsx(Button, { asChild: true, variant: "secondary", size: "sm", children: _jsx(Link, { to: newHornAboutPath(entry.rule.iri), children: "Make a Horn from this" }) })) }), _jsx(Drawn, { entry: entry, datastoreId: datastoreId }), _jsxs(Card, { children: [_jsx(CardTitle, { children: "The rule itself" }), _jsx("pre", { className: "text-muted-foreground overflow-auto px-4 pb-4 font-mono text-xs whitespace-pre-wrap", children: entry.rule.body })] })] }));
74
80
  }
75
81
  export function RulePage() {
76
- const { datastoreId, rule } = useParams();
77
- const { datastores } = useMantaSession();
82
+ const { datastoreId } = useActiveDatastore();
83
+ const { rule } = useParams();
84
+ const { datastores = [] } = useAuthContext();
78
85
  if (!isPresent(datastoreId) || !datastores.some((one) => one.id === datastoreId)) {
79
86
  return _jsx(DatastoreMissing, {});
80
87
  }
81
88
  if (!isPresent(rule))
82
89
  return _jsx(EmptyState, { title: "No rule was named." });
83
- return (_jsx(MantaSparqlProvider, { datastoreId: datastoreId, children: _jsx(Rule, { datastoreId: datastoreId, ruleId: rule }) }));
90
+ return _jsx(Rule, { datastoreId: datastoreId, ruleId: rule });
84
91
  }
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useQuery } from "@tanstack/react-query";
3
3
  import { isPresent } from "@manta-eu/lib/present";
4
- import { Link, useParams } from "react-router";
4
+ import { Link } from "react-router";
5
+ import { useActiveDatastore } from "@manta-eu/react/providers";
5
6
  import { ChevronRight } from "lucide-react";
6
7
  import { Badge, Progress, Skeleton } from "@manta-eu/react";
7
8
  import { rulesQuery, readRules } from "@manta-eu/horns/shapes";
@@ -9,10 +10,9 @@ import { ruleCoverage, ruleViewId } from "@manta-eu/horns/rules";
9
10
  import { rulePath } from "./paths";
10
11
  import { Card, CardTitle, PageHeader } from "../components/page-header";
11
12
  import { DatastoreMissing, ErrorState } from "../components/states";
12
- import { MantaSparqlProvider } from "../lib/manta/providers";
13
- import { useMantaSession } from "../lib/manta/session";
14
- import { browserSparqlSelect } from "../lib/manta/sparql-select";
13
+ import { useSparqlSelect } from "../lib/manta/sparql-select";
15
14
  import { shapesRealm } from "../lib/shapes/catalog";
15
+ import { useAuthContext } from "@manta-eu/react/providers/AuthContext";
16
16
  // The other half of what a datastore knows. A shape says what a thing is and the shapes page covers those;
17
17
  // a rule says what Manta worked out about it, and until this page nothing drew one.
18
18
  function percentOf(part, whole) {
@@ -20,14 +20,14 @@ function percentOf(part, whole) {
20
20
  }
21
21
  function StatusBadge({ entry }) {
22
22
  if (entry.status === "covered") {
23
- return entry.spec?.kind === "plan" ? _jsx(Badge, { variant: "info", children: "Drafts work" }) : _jsx(Badge, { variant: "success", children: "View" });
23
+ return entry.spec?.kind === "plan" ? (_jsx(Badge, { variant: "info", children: "Drafts work" })) : (_jsx(Badge, { variant: "success", children: "View" }));
24
24
  }
25
25
  // Two different gaps: one the parser could close, one Datalog itself will not.
26
26
  if (entry.status === "unparsed")
27
27
  return _jsx(Badge, { variant: "warning", children: "Not read yet" });
28
28
  return _jsx(Badge, { variant: "secondary", children: "No query" });
29
29
  }
30
- function RuleRow({ entry, datastoreId }) {
30
+ function RuleRow({ entry }) {
31
31
  const spec = entry.spec;
32
32
  const derived = spec?.derives === undefined
33
33
  ? undefined
@@ -43,14 +43,14 @@ function RuleRow({ entry, datastoreId }) {
43
43
  .filter((part) => part !== undefined)
44
44
  .join(" · ")
45
45
  : (entry.issues?.[0] ?? "No view.");
46
- return (_jsxs("li", { "data-rule": entry.rule.iri, "data-status": entry.status, className: "flex flex-wrap items-center justify-between gap-3 px-4 py-2.5", children: [_jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "text-default-foreground truncate text-sm font-medium", children: spec?.title ?? entry.rule.iri.slice(entry.rule.iri.lastIndexOf("#") + 1) }), _jsx("span", { className: "text-muted-foreground truncate text-xs", children: said })] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx(StatusBadge, { entry: entry }), spec ? (_jsxs(Link, { className: "text-muted-foreground hover:text-default-foreground flex items-center gap-1 text-sm", to: rulePath(datastoreId, ruleViewId(entry.rule.iri)), children: ["Look at it", _jsx(ChevronRight, { size: 14, "aria-hidden": "true" })] })) : null] })] }));
46
+ return (_jsxs("li", { "data-rule": entry.rule.iri, "data-status": entry.status, className: "flex flex-wrap items-center justify-between gap-3 px-4 py-2.5", children: [_jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "text-default-foreground truncate text-sm font-medium", children: spec?.title ?? entry.rule.iri.slice(entry.rule.iri.lastIndexOf("#") + 1) }), _jsx("span", { className: "text-muted-foreground truncate text-xs", children: said })] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx(StatusBadge, { entry: entry }), spec ? (_jsxs(Link, { className: "text-muted-foreground hover:text-default-foreground flex items-center gap-1 text-sm", to: rulePath(ruleViewId(entry.rule.iri)), children: ["Look at it", _jsx(ChevronRight, { size: 14, "aria-hidden": "true" })] })) : null] })] }));
47
47
  }
48
48
  function Rules({ datastoreId }) {
49
+ const select = useSparqlSelect();
49
50
  const realm = shapesRealm();
50
51
  const rules = useQuery({
51
52
  queryKey: ["rules", datastoreId],
52
53
  queryFn: async ({ signal }) => {
53
- const select = browserSparqlSelect(datastoreId);
54
54
  // Every rule, not a search: this page is the coverage, so it cannot filter by a word.
55
55
  return readRules(await select(rulesQuery(realm, [], allRules), signal));
56
56
  },
@@ -60,15 +60,15 @@ function Rules({ datastoreId }) {
60
60
  if (rules.isError)
61
61
  return _jsx(ErrorState, { title: "The rules could not be read.", hint: rules.error.message });
62
62
  const coverage = ruleCoverage({ rules: rules.data, realm });
63
- return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Card, { children: [_jsx(CardTitle, { children: "Covered" }), _jsxs("div", { className: "flex items-center gap-3 px-4 pb-4", children: [_jsx(Progress, { value: percentOf(coverage.covered, coverage.total), className: "grow" }), _jsx("span", { className: "text-muted-foreground text-sm tabular-nums", children: `${coverage.covered} of ${coverage.total}` })] }), _jsx("p", { className: "text-muted-foreground px-4 pb-4 text-sm", children: `${coverage.plans} of them draft work rather than working a value out.` })] }), _jsxs(Card, { children: [_jsx(CardTitle, { children: "Every rule this datastore runs" }), _jsx("ul", { className: "divide-default-border border-default-border divide-y border-t", children: coverage.entries.map((entry) => (_jsx(RuleRow, { entry: entry, datastoreId: datastoreId }, `${entry.rule.iri}-${entry.rule.body.length}`))) })] })] }));
63
+ return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Card, { children: [_jsx(CardTitle, { children: "Covered" }), _jsxs("div", { className: "flex items-center gap-3 px-4 pb-4", children: [_jsx(Progress, { value: percentOf(coverage.covered, coverage.total), className: "grow" }), _jsx("span", { className: "text-muted-foreground text-sm tabular-nums", children: `${coverage.covered} of ${coverage.total}` })] }), _jsx("p", { className: "text-muted-foreground px-4 pb-4 text-sm", children: `${coverage.plans} of them draft work rather than working a value out.` })] }), _jsxs(Card, { children: [_jsx(CardTitle, { children: "Every rule this datastore runs" }), _jsx("ul", { className: "divide-default-border border-default-border divide-y border-t", children: coverage.entries.map((entry) => (_jsx(RuleRow, { entry: entry }, `${entry.rule.iri}-${entry.rule.body.length}`))) })] })] }));
64
64
  }
65
65
  /** Enough for every rule a realm has; the query caps itself well below what a datastore holds. */
66
66
  const allRules = 200;
67
67
  export function RulesPage() {
68
- const { datastoreId } = useParams();
69
- const { datastores } = useMantaSession();
68
+ const { datastoreId } = useActiveDatastore();
69
+ const { datastores = [] } = useAuthContext();
70
70
  if (!isPresent(datastoreId) || !datastores.some((one) => one.id === datastoreId)) {
71
71
  return _jsx(DatastoreMissing, {});
72
72
  }
73
- return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(PageHeader, { title: "Rules", description: "What Manta works out about this datastore, and whether it can be looked at." }), _jsx(MantaSparqlProvider, { datastoreId: datastoreId, children: _jsx(Rules, { datastoreId: datastoreId }) })] }));
73
+ return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(PageHeader, { title: "Rules", description: "What Manta works out about this datastore, and whether it can be looked at." }), _jsx(Rules, { datastoreId: datastoreId })] }));
74
74
  }
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useQuery } from "@tanstack/react-query";
3
3
  import { isPresent } from "@manta-eu/lib/present";
4
4
  import { Link, useParams, useSearchParams } from "react-router";
5
+ import { useActiveDatastore } from "@manta-eu/react/providers";
5
6
  import { shapeViewSpecs } from "@manta-eu/horns-kit";
6
7
  import { buildShapeDetail, shapeLinks } from "@manta-eu/horns/shapes";
7
8
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@manta-eu/react";
@@ -9,9 +10,7 @@ import { shapeZooPath } from "./paths";
9
10
  import { SnapshotViewBody } from "../components/snapshot-view";
10
11
  import { PageHeader } from "../components/page-header";
11
12
  import { DatastoreMissing, EmptyState, ErrorState, Loading } from "../components/states";
12
- import { MantaSparqlProvider } from "../lib/manta/providers";
13
- import { useMantaSession } from "../lib/manta/session";
14
- import { browserSparqlSelect } from "../lib/manta/sparql-select";
13
+ import { useSparqlSelect } from "../lib/manta/sparql-select";
15
14
  import { ldkitShapeCatalog, shapesRealm } from "../lib/shapes/catalog";
16
15
  const specOf = (schema) => shapeViewSpecs.find((spec) => spec.schema === schema);
17
16
  function Section({ page }) {
@@ -21,6 +20,7 @@ function Section({ page }) {
21
20
  return _jsx(SnapshotViewBody, { snapshot: page.snapshot, view: { ...view, title: page.section.title } });
22
21
  }
23
22
  function Detail({ datastoreId, schema, entity }) {
23
+ const select = useSparqlSelect();
24
24
  const realm = shapesRealm();
25
25
  const shape = ldkitShapeCatalog().shapes.find((candidate) => candidate.schema === schema);
26
26
  const spec = specOf(schema);
@@ -30,7 +30,6 @@ function Detail({ datastoreId, schema, entity }) {
30
30
  queryFn: async ({ signal }) => {
31
31
  if (!shape || !spec)
32
32
  throw new Error("The shape has no view to open.");
33
- const select = browserSparqlSelect(datastoreId);
34
33
  const links = await shapeLinks({ shape, shapes: ldkitShapeCatalog().shapes, realm, select, signal });
35
34
  return buildShapeDetail({ spec, links, specOf, realm, datastoreId, entity, select, signal });
36
35
  },
@@ -57,12 +56,11 @@ function Detail({ datastoreId, schema, entity }) {
57
56
  return (_jsxs(Tabs, { defaultValue: first.section.id, children: [_jsx(TabsList, { className: "flex flex-wrap", children: pages.map((page) => (_jsx(TabsTrigger, { value: page.section.id, children: page.section.title }, page.section.id))) }), pages.map((page) => (_jsx(TabsContent, { value: page.section.id, "data-detail-section": page.section.id, children: _jsx(Section, { page: page }) }, page.section.id)))] }));
58
57
  }
59
58
  export function ShapeEntityPage() {
60
- const { datastoreId, schema } = useParams();
59
+ const { datastoreId } = useActiveDatastore();
60
+ const { schema } = useParams();
61
61
  const [params] = useSearchParams();
62
62
  const entity = params.get("iri");
63
- const { datastores } = useMantaSession();
64
- const datastore = datastores.find((candidate) => candidate.id === datastoreId);
65
- if (!isPresent(datastore) || !isPresent(schema) || !isPresent(entity))
63
+ if (!isPresent(datastoreId) || !isPresent(schema) || !isPresent(entity))
66
64
  return _jsx(DatastoreMissing, {});
67
- return (_jsx(MantaSparqlProvider, { datastoreId: datastore.id, children: _jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: schema.replace(/Schema$/, ""), description: entity, actions: _jsx(Link, { className: "text-sm text-muted-foreground hover:text-default-foreground", to: shapeZooPath(datastore.id, schema), children: "Back to zoo" }) }), _jsx(Detail, { datastoreId: datastore.id, schema: schema, entity: entity })] }) }));
65
+ return (_jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: schema.replace(/Schema$/, ""), description: entity, actions: _jsx(Link, { className: "text-sm text-muted-foreground hover:text-default-foreground", to: shapeZooPath(schema), children: "Back to zoo" }) }), _jsx(Detail, { datastoreId: datastoreId, schema: schema, entity: entity })] }));
68
66
  }
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useQuery } from "@tanstack/react-query";
3
3
  import { isPresent } from "@manta-eu/lib/present";
4
4
  import { Link, useParams } from "react-router";
5
+ import { useActiveDatastore } from "@manta-eu/react/providers";
5
6
  import { ChevronRight } from "lucide-react";
6
7
  import { Badge } from "@manta-eu/react";
7
8
  import { shapeViewSpecs } from "@manta-eu/horns-kit";
@@ -11,9 +12,7 @@ import { ShapeViewStudio } from "./shape-view-studio";
11
12
  import { SnapshotViewBody } from "../components/snapshot-view";
12
13
  import { PageHeader } from "../components/page-header";
13
14
  import { DatastoreMissing, EmptyState, ErrorState, Loading } from "../components/states";
14
- import { MantaSparqlProvider } from "../lib/manta/providers";
15
- import { useMantaSession } from "../lib/manta/session";
16
- import { browserSparqlSelect } from "../lib/manta/sparql-select";
15
+ import { useSparqlSelect } from "../lib/manta/sparql-select";
17
16
  import { ldkitShapeCatalog, shapesRealm } from "../lib/shapes/catalog";
18
17
  function Problems({ heading, problems }) {
19
18
  if (!isPresent(problems))
@@ -21,13 +20,14 @@ function Problems({ heading, problems }) {
21
20
  return (_jsxs("div", { role: "alert", "data-problems": true, className: "rounded-lg border border-error bg-error/10 px-4 py-3 text-sm", children: [_jsx("p", { className: "font-medium text-default-foreground", children: heading }), _jsx("ul", { className: "mt-2 flex list-disc flex-col gap-1 pl-5 text-muted-foreground", children: problems.map((problem) => (_jsx("li", { children: problem }, problem))) })] }));
22
21
  }
23
22
  function Zoo(props) {
23
+ const select = useSparqlSelect();
24
24
  const { datastoreId, spec, version } = props;
25
25
  const realm = shapesRealm();
26
26
  const zoo = useQuery({
27
27
  // Keyed on the version: a changed view is a different zoo, and returning the cached one would show the
28
28
  // card the person just moved away from.
29
29
  queryKey: ["shape-zoo", datastoreId, spec.schema, version],
30
- queryFn: ({ signal }) => buildShapeZoo({ spec, realm, datastoreId, select: browserSparqlSelect(datastoreId), signal }),
30
+ queryFn: ({ signal }) => buildShapeZoo({ spec, realm, datastoreId, select: select, signal }),
31
31
  });
32
32
  if (zoo.isPending)
33
33
  return _jsx(Loading, { label: "Sampling instances\u2026", rows: 4 });
@@ -39,20 +39,19 @@ function Zoo(props) {
39
39
  if (sampled === 0) {
40
40
  return (_jsx(EmptyState, { title: `This datastore holds no ${spec.schema.replace(/Schema$/, "")} instances yet.`, hint: "A zoo needs real data to show the variants a view has to survive." }));
41
41
  }
42
- return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", "data-zoo-summary": true, children: `Sampled ${sampled} instances · ${distinct} distinct variants · showing ${variants.length}` }), _jsx(Problems, { heading: "What the zoo shows is wrong with this view", problems: [...zooProblems(zoo.data, spec), ...summarizeZooFindings(auditShapeZoo(zoo.data))] }), _jsxs("details", { className: "group rounded-lg border border-default-border bg-card", children: [_jsxs("summary", { className: "flex cursor-pointer list-none items-center gap-3 px-4 py-3", children: [_jsx(ChevronRight, { size: 16, "aria-hidden": "true", className: "shrink-0 text-muted-foreground transition-transform group-open:rotate-90" }), _jsx("span", { className: "font-medium text-default-foreground", children: "How often each field is filled" })] }), _jsx("ul", { className: "divide-y divide-default-border border-t border-default-border", children: spec.fields.map((field) => (_jsxs("li", { className: "flex items-center justify-between gap-4 px-4 py-1.5 text-sm", children: [_jsx("span", { className: "font-mono text-default-foreground", children: field.name }), _jsx("span", { className: "text-muted-foreground", children: `${Math.round((fillRates[field.name] ?? 0) * 100)}%` })] }, field.name))) })] }), content?.lane === "error" ? _jsx(ErrorState, { title: "The view failed.", hint: content.message }) : null, _jsx("div", { className: "grid gap-4 [grid-template-columns:repeat(auto-fit,minmax(22rem,1fr))]", children: variants.map((variant, index) => (_jsxs("section", { "data-zoo-variant": variant.entity, className: "flex flex-col gap-2 rounded-lg border border-default-border bg-card p-4", children: [_jsxs("header", { className: "flex flex-col gap-1", children: [_jsx("h2", { className: "text-sm font-medium", children: _jsx(Link, { className: "text-default-foreground hover:underline", to: shapeEntityPath(datastoreId, spec.schema, variant.entity), children: `Variant ${index + 1}` }) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [`${variant.populated.length} of ${spec.fields.length} fields · ${variant.lookalikes} ${variant.lookalikes === 1 ? "instance" : "instances"} like this`, isPresent(variant.missing) ? ` · no ${variant.missing.join(", ")}` : ""] })] }), view && content?.lane === "sandbox" ? (_jsx(SnapshotViewBody, { snapshot: snapshot, view: {
42
+ return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", "data-zoo-summary": true, children: `Sampled ${sampled} instances · ${distinct} distinct variants · showing ${variants.length}` }), _jsx(Problems, { heading: "What the zoo shows is wrong with this view", problems: [...zooProblems(zoo.data, spec), ...summarizeZooFindings(auditShapeZoo(zoo.data))] }), _jsxs("details", { className: "group rounded-lg border border-default-border bg-card", children: [_jsxs("summary", { className: "flex cursor-pointer list-none items-center gap-3 px-4 py-3", children: [_jsx(ChevronRight, { size: 16, "aria-hidden": "true", className: "shrink-0 text-muted-foreground transition-transform group-open:rotate-90" }), _jsx("span", { className: "font-medium text-default-foreground", children: "How often each field is filled" })] }), _jsx("ul", { className: "divide-y divide-default-border border-t border-default-border", children: spec.fields.map((field) => (_jsxs("li", { className: "flex items-center justify-between gap-4 px-4 py-1.5 text-sm", children: [_jsx("span", { className: "font-mono text-default-foreground", children: field.name }), _jsx("span", { className: "text-muted-foreground", children: `${Math.round((fillRates[field.name] ?? 0) * 100)}%` })] }, field.name))) })] }), content?.lane === "error" ? _jsx(ErrorState, { title: "The view failed.", hint: content.message }) : null, _jsx("div", { className: "grid gap-4 [grid-template-columns:repeat(auto-fit,minmax(22rem,1fr))]", children: variants.map((variant, index) => (_jsxs("section", { "data-zoo-variant": variant.entity, className: "flex flex-col gap-2 rounded-lg border border-default-border bg-card p-4", children: [_jsxs("header", { className: "flex flex-col gap-1", children: [_jsx("h2", { className: "text-sm font-medium", children: _jsx(Link, { className: "text-default-foreground hover:underline", to: shapeEntityPath(spec.schema, variant.entity), children: `Variant ${index + 1}` }) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [`${variant.populated.length} of ${spec.fields.length} fields · ${variant.lookalikes} ${variant.lookalikes === 1 ? "instance" : "instances"} like this`, isPresent(variant.missing) ? ` · no ${variant.missing.join(", ")}` : ""] })] }), view && content?.lane === "sandbox" ? (_jsx(SnapshotViewBody, { snapshot: snapshot, view: {
43
43
  ...view,
44
44
  title: `${spec.schema} variant ${index + 1}`,
45
45
  content: { ...content, rows: content.rows.filter((row) => row["this"] === variant.entity) },
46
46
  } })) : null] }, variant.entity))) })] }));
47
47
  }
48
48
  export function ShapeZooPage() {
49
- const { datastoreId, schema } = useParams();
50
- const { datastores } = useMantaSession();
51
- const datastore = datastores.find((candidate) => candidate.id === datastoreId);
52
- if (!isPresent(datastore) || !isPresent(schema))
49
+ const { datastoreId } = useActiveDatastore();
50
+ const { schema } = useParams();
51
+ if (!isPresent(datastoreId) || !isPresent(schema))
53
52
  return _jsx(DatastoreMissing, {});
54
53
  const realm = shapesRealm();
55
54
  const shape = ldkitShapeCatalog().shapes.find((candidate) => candidate.schema === schema);
56
55
  const spec = shapeViewSpecs.find((candidate) => candidate.schema === schema);
57
- return (_jsx(MantaSparqlProvider, { datastoreId: datastore.id, children: _jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: `${schema} zoo`, description: "Real instances of this shape, one per variant of the fields it fills. Every card must survive all of them.", meta: spec ? (_jsx(Badge, { variant: spec.status === "curated" ? "success" : "info", children: `${spec.classCurie} · ${spec.status}` })) : undefined, actions: _jsx(Link, { className: "text-sm text-muted-foreground hover:text-default-foreground", to: shapesPath(datastore.id), children: "Back to shapes" }) }), !shape ? (_jsx(ErrorState, { title: `${schema} is not a shape the SDK knows.` })) : !spec ? (_jsx(EmptyState, { title: `${shape.schema} has no HornView yet.`, hint: "Run the Studio over the coverage worklist to draft one." })) : (_jsx(ShapeViewStudio, { datastoreId: datastore.id, stored: spec, children: (current, version) => (_jsxs(_Fragment, { children: [_jsx(Problems, { heading: "This view does not fit its shape", problems: shapeViewProblems(current, shape, realm) }), _jsx(Zoo, { datastoreId: datastore.id, spec: current, version: version })] })) }))] }) }));
56
+ return (_jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: `${schema} zoo`, description: "Real instances of this shape, one per variant of the fields it fills. Every card must survive all of them.", meta: spec ? (_jsx(Badge, { variant: spec.status === "curated" ? "success" : "info", children: `${spec.classCurie} · ${spec.status}` })) : undefined, actions: _jsx(Link, { className: "text-sm text-muted-foreground hover:text-default-foreground", to: shapesPath(), children: "Back to shapes" }) }), !shape ? (_jsx(ErrorState, { title: `${schema} is not a shape the SDK knows.` })) : !spec ? (_jsx(EmptyState, { title: `${shape.schema} has no HornView yet.`, hint: "Run the Studio over the coverage worklist to draft one." })) : (_jsx(ShapeViewStudio, { datastoreId: datastoreId, stored: spec, children: (current, version) => (_jsxs(_Fragment, { children: [_jsx(Problems, { heading: "This view does not fit its shape", problems: shapeViewProblems(current, shape, realm) }), _jsx(Zoo, { datastoreId: datastoreId, spec: current, version: version })] })) }))] }));
58
57
  }
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useQuery } from "@tanstack/react-query";
3
3
  import { isPresent } from "@manta-eu/lib/present";
4
- import { Link, useParams } from "react-router";
4
+ import { Link } from "react-router";
5
+ import { useActiveDatastore } from "@manta-eu/react/providers";
5
6
  import { ChevronRight } from "lucide-react";
6
7
  import { Badge, Progress, Skeleton } from "@manta-eu/react";
7
8
  import { compactIri } from "@manta-eu/horns";
@@ -11,9 +12,7 @@ import { CoverageRun } from "./coverage-run";
11
12
  import { shapeZooPath } from "./paths";
12
13
  import { Card, CardTitle, PageHeader } from "../components/page-header";
13
14
  import { DatastoreMissing, ErrorState } from "../components/states";
14
- import { MantaSparqlProvider } from "../lib/manta/providers";
15
- import { useMantaSession } from "../lib/manta/session";
16
- import { browserSparqlSelect } from "../lib/manta/sparql-select";
15
+ import { useSparqlSelect } from "../lib/manta/sparql-select";
17
16
  import { ldkitShapeCatalog, shapesRealm } from "../lib/shapes/catalog";
18
17
  function percentOf(part, whole) {
19
18
  return whole === 0 ? 0 : Math.round((part / whole) * 1000) / 10;
@@ -27,16 +26,17 @@ function StatusBadge({ status }) {
27
26
  return _jsx(Badge, { variant: "info", children: "Drafted" });
28
27
  return _jsx(Badge, { variant: "secondary", children: "No view" });
29
28
  }
30
- function ShapeRow({ entry, realm, datastoreId, }) {
29
+ function ShapeRow({ entry, realm }) {
31
30
  const shape = entry.shape;
32
- return (_jsxs("li", { "data-shape": shape.schema, "data-status": entry.status, className: "flex flex-wrap items-center justify-between gap-3 px-4 py-2.5", children: [_jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-mono text-sm text-default-foreground", children: shape.schema }), _jsxs("span", { className: "truncate text-xs text-muted-foreground", children: [`${compactIri(realm, shape.classIri)} · ${shape.properties.length} properties`, entry.observed ? " · in this datastore" : ""] })] }), _jsxs("span", { className: "flex shrink-0 items-center gap-3", children: [_jsx(StatusBadge, { status: entry.status }), entry.status === "missing" ? null : (_jsx(Link, { className: "text-sm text-muted-foreground underline hover:text-default-foreground", to: shapeZooPath(datastoreId, shape.schema), children: entry.status === "curated" ? "Zoo" : "Zoo (draft)" }))] })] }));
31
+ return (_jsxs("li", { "data-shape": shape.schema, "data-status": entry.status, className: "flex flex-wrap items-center justify-between gap-3 px-4 py-2.5", children: [_jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-mono text-sm text-default-foreground", children: shape.schema }), _jsxs("span", { className: "truncate text-xs text-muted-foreground", children: [`${compactIri(realm, shape.classIri)} · ${shape.properties.length} properties`, entry.observed ? " · in this datastore" : ""] })] }), _jsxs("span", { className: "flex shrink-0 items-center gap-3", children: [_jsx(StatusBadge, { status: entry.status }), entry.status === "missing" ? null : (_jsx(Link, { className: "text-sm text-muted-foreground underline hover:text-default-foreground", to: shapeZooPath(shape.schema), children: entry.status === "curated" ? "Zoo" : "Zoo (draft)" }))] })] }));
33
32
  }
34
33
  function ShapeCoverageDashboard({ datastoreId }) {
34
+ const select = useSparqlSelect();
35
35
  const realm = shapesRealm();
36
36
  const catalog = ldkitShapeCatalog();
37
37
  const observed = useQuery({
38
38
  queryKey: ["observed-classes", datastoreId],
39
- queryFn: async ({ signal }) => readObservedClasses(await browserSparqlSelect(datastoreId)(observedClassesQuery(realm), signal)),
39
+ queryFn: async ({ signal }) => readObservedClasses(await select(observedClassesQuery(realm), signal)),
40
40
  });
41
41
  // The queue is ordered by how much data a class has, so the views worth building come first. Counted in one
42
42
  // query, bounded by the classes this datastore actually holds.
@@ -47,7 +47,7 @@ function ShapeCoverageDashboard({ datastoreId }) {
47
47
  const held = catalog.shapes.filter((shape) => observed.data?.has(shape.classIri)).map((shape) => shape.classIri);
48
48
  if (!isPresent(held))
49
49
  return new Map();
50
- return readInstanceCounts(await browserSparqlSelect(datastoreId)(instanceCountsQuery(held), signal));
50
+ return readInstanceCounts(await select(instanceCountsQuery(held), signal));
51
51
  },
52
52
  });
53
53
  const coverage = shapeCoverage({
@@ -61,15 +61,13 @@ function ShapeCoverageDashboard({ datastoreId }) {
61
61
  const percent = percentOf(coverage.covered, coverage.total);
62
62
  return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Card, { children: [_jsxs("section", { "aria-label": "Coverage summary", className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex flex-wrap items-baseline gap-3", children: [_jsx("span", { className: "text-4xl font-semibold text-default-foreground", children: `${coverage.covered} / ${coverage.total}` }), _jsx("span", { className: "text-sm text-muted-foreground", children: "shapes have a HornView" }), _jsx("span", { className: "ml-auto text-2xl font-semibold text-default-foreground", children: `${percent}%` })] }), _jsx(Progress, { value: percent, "aria-label": "Shapes covered", "aria-valuenow": percent, "aria-valuetext": `${percent}% of shapes have a HornView` }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [`${coverage.curated} curated after review in a zoo`, coverage.observed
63
63
  ? ` · ${coverage.observed.covered} / ${coverage.observed.total} of the shapes this datastore holds`
64
- : ""] }), observed.isPending ? _jsx(Skeleton, { className: "h-4 w-64" }) : null] }), observed.isError ? (_jsx("div", { className: "pt-3", children: _jsx(ErrorState, { title: "Could not read which shapes this datastore holds.", hint: observed.error.message }) })) : null] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [isPresent(drafted) ? (_jsx(Card, { children: _jsxs("section", { "aria-labelledby": "review", className: "flex flex-col gap-3", children: [_jsx(CardTitle, { aside: `${drafted.length} to review`, children: _jsx("span", { id: "review", children: "Drafted, waiting on a person" }) }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Each of these was designed by the Studio and has not been looked at. Open its zoo: what the audit reports there is what to fix, and a view that holds up is promoted to curated." }), _jsx("ul", { "data-review": true, className: "flex flex-wrap gap-2", children: drafted.slice(0, maxReviewLinks).map((entry) => (_jsx("li", { children: _jsx(Link, { "data-review-shape": entry.shape.schema, className: "rounded-md border border-default-border px-2 py-1 font-mono text-xs text-muted-foreground hover:bg-card/70 hover:text-default-foreground", to: shapeZooPath(datastoreId, entry.shape.schema), children: entry.shape.schema }) }, entry.shape.schema))) }), drafted.length > maxReviewLinks ? (_jsx("p", { className: "text-xs text-muted-foreground", children: `…${drafted.length - maxReviewLinks} more, in the list below.` })) : null] }) })) : null, isPresent(worklist) ? (_jsx(Card, { children: _jsxs("section", { "aria-labelledby": "worklist", className: "flex flex-col gap-3", children: [_jsx(CardTitle, { aside: `${worklist.length} next`, children: _jsx("span", { id: "worklist", children: "Next for the Studio" }) }), _jsx("ol", { className: "flex flex-col divide-y divide-default-border text-sm", children: worklist.map((entry) => (_jsxs("li", { className: "flex items-center justify-between gap-4 py-2", children: [_jsx("span", { className: "truncate font-mono text-default-foreground", children: entry.shape.schema }), _jsxs("span", { className: "shrink-0 text-xs text-muted-foreground", children: [entry.status === "drafted" ? "review the draft in its zoo" : "needs a view", entry.instances === undefined
64
+ : ""] }), observed.isPending ? _jsx(Skeleton, { className: "h-4 w-64" }) : null] }), observed.isError ? (_jsx("div", { className: "pt-3", children: _jsx(ErrorState, { title: "Could not read which shapes this datastore holds.", hint: observed.error.message }) })) : null] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [isPresent(drafted) ? (_jsx(Card, { children: _jsxs("section", { "aria-labelledby": "review", className: "flex flex-col gap-3", children: [_jsx(CardTitle, { aside: `${drafted.length} to review`, children: _jsx("span", { id: "review", children: "Drafted, waiting on a person" }) }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Each of these was designed by the Studio and has not been looked at. Open its zoo: what the audit reports there is what to fix, and a view that holds up is promoted to curated." }), _jsx("ul", { "data-review": true, className: "flex flex-wrap gap-2", children: drafted.slice(0, maxReviewLinks).map((entry) => (_jsx("li", { children: _jsx(Link, { "data-review-shape": entry.shape.schema, className: "rounded-md border border-default-border px-2 py-1 font-mono text-xs text-muted-foreground hover:bg-card/70 hover:text-default-foreground", to: shapeZooPath(entry.shape.schema), children: entry.shape.schema }) }, entry.shape.schema))) }), drafted.length > maxReviewLinks ? (_jsx("p", { className: "text-xs text-muted-foreground", children: `…${drafted.length - maxReviewLinks} more, in the list below.` })) : null] }) })) : null, isPresent(worklist) ? (_jsx(Card, { children: _jsxs("section", { "aria-labelledby": "worklist", className: "flex flex-col gap-3", children: [_jsx(CardTitle, { aside: `${worklist.length} next`, children: _jsx("span", { id: "worklist", children: "Next for the Studio" }) }), _jsx("ol", { className: "flex flex-col divide-y divide-default-border text-sm", children: worklist.map((entry) => (_jsxs("li", { className: "flex items-center justify-between gap-4 py-2", children: [_jsx("span", { className: "truncate font-mono text-default-foreground", children: entry.shape.schema }), _jsxs("span", { className: "shrink-0 text-xs text-muted-foreground", children: [entry.status === "drafted" ? "review the draft in its zoo" : "needs a view", entry.instances === undefined
65
65
  ? ""
66
- : ` · ${entry.instances.toLocaleString("en")} instance${entry.instances === 1 ? "" : "s"}`] })] }, entry.shape.schema))) })] }) })) : null] }), _jsx(CoverageRun, { datastoreId: datastoreId }), _jsx("section", { "aria-label": "Shapes by namespace", className: "flex flex-col gap-2", children: coverage.namespaces.map((group) => (_jsxs("details", { "data-namespace": group.namespace, className: "group rounded-lg border border-default-border bg-card", children: [_jsxs("summary", { className: "flex cursor-pointer list-none items-center gap-3 px-4 py-3", children: [_jsx(ChevronRight, { size: 16, "aria-hidden": "true", className: "shrink-0 text-muted-foreground transition-transform group-open:rotate-90" }), _jsx("span", { className: "font-medium text-default-foreground", children: group.namespace }), _jsx("span", { className: "text-sm text-muted-foreground", children: `${group.covered} / ${group.total}` }), _jsx("span", { className: "ml-auto text-sm text-muted-foreground", children: `${percentOf(group.covered, group.total)}%` })] }), _jsx("ul", { className: "divide-y divide-default-border border-t border-default-border", children: group.shapes.map((entry) => (_jsx(ShapeRow, { entry: entry, realm: realm, datastoreId: datastoreId }, entry.shape.schema))) })] }, group.namespace))) })] }));
66
+ : ` · ${entry.instances.toLocaleString("en")} instance${entry.instances === 1 ? "" : "s"}`] })] }, entry.shape.schema))) })] }) })) : null] }), _jsx(CoverageRun, { datastoreId: datastoreId }), _jsx("section", { "aria-label": "Shapes by namespace", className: "flex flex-col gap-2", children: coverage.namespaces.map((group) => (_jsxs("details", { "data-namespace": group.namespace, className: "group rounded-lg border border-default-border bg-card", children: [_jsxs("summary", { className: "flex cursor-pointer list-none items-center gap-3 px-4 py-3", children: [_jsx(ChevronRight, { size: 16, "aria-hidden": "true", className: "shrink-0 text-muted-foreground transition-transform group-open:rotate-90" }), _jsx("span", { className: "font-medium text-default-foreground", children: group.namespace }), _jsx("span", { className: "text-sm text-muted-foreground", children: `${group.covered} / ${group.total}` }), _jsx("span", { className: "ml-auto text-sm text-muted-foreground", children: `${percentOf(group.covered, group.total)}%` })] }), _jsx("ul", { className: "divide-y divide-default-border border-t border-default-border", children: group.shapes.map((entry) => (_jsx(ShapeRow, { entry: entry, realm: realm }, entry.shape.schema))) })] }, group.namespace))) })] }));
67
67
  }
68
68
  export function ShapesPage() {
69
- const { datastoreId } = useParams();
70
- const { datastores } = useMantaSession();
71
- const datastore = datastores.find((candidate) => candidate.id === datastoreId);
72
- if (!isPresent(datastore))
69
+ const { datastoreId } = useActiveDatastore();
70
+ if (!isPresent(datastoreId))
73
71
  return _jsx(DatastoreMissing, {});
74
- return (_jsx(MantaSparqlProvider, { datastoreId: datastore.id, children: _jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: "Shape coverage", description: "Every SHACL-derived LDKit shape should have a HornView. A covered shape opens a zoo of the variants its real instances in this datastore take." }), _jsx(ShapeCoverageDashboard, { datastoreId: datastore.id })] }) }));
72
+ return (_jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: "Shape coverage", description: "Every SHACL-derived LDKit shape should have a HornView. A covered shape opens a zoo of the variants its real instances in this datastore take." }), _jsx(ShapeCoverageDashboard, { datastoreId: datastoreId })] }));
75
73
  }
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { useQueries, useQuery } from "@tanstack/react-query";
4
4
  import { isPresent } from "@manta-eu/lib/present";
5
- import { Link, useParams } from "react-router";
5
+ import { Link } from "react-router";
6
+ import { useActiveDatastore } from "@manta-eu/react/providers";
6
7
  import { Columns3, Plus } from "lucide-react";
7
8
  import { Button, cn } from "@manta-eu/react";
8
9
  import { hornSchema, hornSources, isRealm } from "@manta-eu/horns";
@@ -10,8 +11,6 @@ import { PageHeader } from "../components/page-header";
10
11
  import { DatastoreMissing, EmptyState, ErrorState, Loading } from "../components/states";
11
12
  import { hornViewPath, newHornPath } from "./paths";
12
13
  import { isRecord } from "@manta-eu/horns";
13
- import { MantaSparqlProvider } from "../lib/manta/providers";
14
- import { useMantaSession } from "../lib/manta/session";
15
14
  import { timeAgo } from "../lib/time-ago";
16
15
  // Every Horn this datastore holds, as a card showing what it looked like when someone last saved it, what it
17
16
  // answers, who it is for and what it reads — so a person finds one by recognising it rather than by
@@ -44,12 +43,10 @@ function screenshotSrc(datastoreId, fileIri) {
44
43
  return `/d/${encodeURIComponent(datastoreId)}/redirect?iri=${encodeURIComponent(fileIri)}`;
45
44
  }
46
45
  export function ShowcasePage() {
47
- const { datastoreId } = useParams();
48
- const { datastores } = useMantaSession();
49
- const datastore = datastores.find((candidate) => candidate.id === datastoreId);
50
- if (!isPresent(datastore))
46
+ const { datastoreId } = useActiveDatastore();
47
+ if (!isPresent(datastoreId))
51
48
  return _jsx(DatastoreMissing, {});
52
- return (_jsx(MantaSparqlProvider, { datastoreId: datastore.id, children: _jsx(Showcase, { datastoreId: datastore.id }) }));
49
+ return _jsx(Showcase, { datastoreId: datastoreId });
53
50
  }
54
51
  function Showcase({ datastoreId }) {
55
52
  const [showing, setShowing] = useState(everyone);
@@ -90,10 +87,10 @@ function Showcase({ datastoreId }) {
90
87
  ];
91
88
  // A Horn whose document has not arrived is not yet known to be of another audience, so it stays shown.
92
89
  const shown = showing === everyone ? cards : cards.filter((card) => card.details?.audience === showing);
93
- return (_jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: "Showcase", description: "What this datastore can already answer. Open one to look at it, or describe a new one in your own words.", actions: _jsx(Button, { variant: "primary", asChild: true, children: _jsxs(Link, { to: newHornPath(datastoreId), children: [_jsx(Plus, { size: 16, "aria-hidden": "true" }), "New Horn"] }) }) }), audiences.length > 1 ? (_jsx("div", { "data-audiences": true, className: "flex flex-wrap gap-2", children: audiences.map((audience) => (_jsx("button", { type: "button", "aria-pressed": audience === showing, onClick: () => setShowing(audience), className: cn("rounded-full border px-3 py-1 text-xs transition-colors", audience === showing
90
+ return (_jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: "Showcase", description: "What this datastore can already answer. Open one to look at it, or describe a new one in your own words.", actions: _jsx(Button, { variant: "primary", asChild: true, children: _jsxs(Link, { to: newHornPath(), children: [_jsx(Plus, { size: 16, "aria-hidden": "true" }), "New Horn"] }) }) }), audiences.length > 1 ? (_jsx("div", { "data-audiences": true, className: "flex flex-wrap gap-2", children: audiences.map((audience) => (_jsx("button", { type: "button", "aria-pressed": audience === showing, onClick: () => setShowing(audience), className: cn("rounded-full border px-3 py-1 text-xs transition-colors", audience === showing
94
91
  ? "border-default-border bg-muted font-medium text-default-foreground"
95
- : "border-muted-border text-muted-foreground hover:text-default-foreground"), children: audience }, audience))) })) : null, listed.isPending ? (_jsx(Loading, { label: "Reading this datastore's Horns\u2026", rows: 3 })) : listed.isError ? (_jsx(ErrorState, { title: "The showcase could not be loaded", hint: "Try again in a moment. If it keeps happening, the data service may be down." })) : !isPresent(shown) ? (_jsx(EmptyState, { title: "Nothing here 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(datastoreId), children: [_jsx(Plus, { size: 14, "aria-hidden": "true" }), "Make your first Horn"] }) }) })) : (_jsx("ul", { "data-showcase": true, className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: shown.map((card) => {
92
+ : "border-muted-border text-muted-foreground hover:text-default-foreground"), children: audience }, audience))) })) : null, listed.isPending ? (_jsx(Loading, { label: "Reading this datastore's Horns\u2026", rows: 3 })) : listed.isError ? (_jsx(ErrorState, { title: "The showcase could not be loaded", hint: "Try again in a moment. If it keeps happening, the data service may be down." })) : !isPresent(shown) ? (_jsx(EmptyState, { title: "Nothing here 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-showcase": true, className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: shown.map((card) => {
96
93
  const age = card.capturedAt === undefined ? undefined : timeAgo(card.capturedAt);
97
- return (_jsx("li", { children: _jsxs(Link, { "data-showcase-card": card.slug, to: hornViewPath(datastoreId, card.slug), className: "flex h-full flex-col gap-3 rounded-xl border border-default-border bg-card p-4 transition-colors hover:bg-card/70", children: [card.screenshot === undefined ? null : (_jsx("span", { "data-showcase-shot": true, className: "block overflow-hidden rounded-lg border border-muted-border bg-muted", style: { aspectRatio: "16 / 10" }, children: _jsx("img", { src: screenshotSrc(datastoreId, card.screenshot), alt: "", loading: "lazy", className: "h-full w-full object-cover object-top" }) })), _jsxs("span", { className: "flex items-start justify-between gap-3", children: [_jsx("span", { className: "text-sm font-medium text-default-foreground", children: card.title }), card.details?.audience ? (_jsx("span", { "data-audience": card.details.audience, className: "shrink-0 rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground", children: card.details.audience })) : null] }), card.details?.summary ? (_jsx("span", { className: "text-xs leading-relaxed text-muted-foreground", children: card.details.summary })) : null, _jsxs("span", { className: "mt-auto flex flex-col gap-1.5 pt-1", children: [isPresent(card.details?.sources) ? (_jsxs("span", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-[11px] text-muted-foreground", children: "Reads" }), card.details.sources.map((source) => (_jsx("span", { className: "rounded-md border border-muted-border px-1.5 py-0.5 text-[11px] text-default-foreground", children: source }, source)))] })) : null, age === undefined ? null : (_jsx("span", { "data-showcase-age": true, className: "text-[11px] text-muted-foreground", children: `as of ${age}` }))] })] }) }, card.slug));
94
+ return (_jsx("li", { children: _jsxs(Link, { "data-showcase-card": card.slug, to: hornViewPath(card.slug), className: "flex h-full flex-col gap-3 rounded-xl border border-default-border bg-card p-4 transition-colors hover:bg-card/70", children: [card.screenshot === undefined ? null : (_jsx("span", { "data-showcase-shot": true, className: "block overflow-hidden rounded-lg border border-muted-border bg-muted", style: { aspectRatio: "16 / 10" }, children: _jsx("img", { src: screenshotSrc(datastoreId, card.screenshot), alt: "", loading: "lazy", className: "h-full w-full object-cover object-top" }) })), _jsxs("span", { className: "flex items-start justify-between gap-3", children: [_jsx("span", { className: "text-sm font-medium text-default-foreground", children: card.title }), card.details?.audience ? (_jsx("span", { "data-audience": card.details.audience, className: "shrink-0 rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground", children: card.details.audience })) : null] }), card.details?.summary ? (_jsx("span", { className: "text-xs leading-relaxed text-muted-foreground", children: card.details.summary })) : null, _jsxs("span", { className: "mt-auto flex flex-col gap-1.5 pt-1", children: [isPresent(card.details?.sources) ? (_jsxs("span", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-[11px] text-muted-foreground", children: "Reads" }), card.details.sources.map((source) => (_jsx("span", { className: "rounded-md border border-muted-border px-1.5 py-0.5 text-[11px] text-default-foreground", children: source }, source)))] })) : null, age === undefined ? null : (_jsx("span", { "data-showcase-age": true, className: "text-[11px] text-muted-foreground", children: `as of ${age}` }))] })] }) }, card.slug));
98
95
  }) }))] }));
99
96
  }