@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/routes.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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 }) });
|
package/dist/app/rule-page.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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:
|
|
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
|
|
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" }) } : {})
|
|
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
|
|
77
|
-
const {
|
|
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
|
|
90
|
+
return _jsx(Rule, { datastoreId: datastoreId, ruleId: rule });
|
|
84
91
|
}
|
package/dist/app/rules-page.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
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(
|
|
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
|
|
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 } =
|
|
69
|
-
const { datastores } =
|
|
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(
|
|
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 {
|
|
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
|
|
59
|
+
const { datastoreId } = useActiveDatastore();
|
|
60
|
+
const { schema } = useParams();
|
|
61
61
|
const [params] = useSearchParams();
|
|
62
62
|
const entity = params.get("iri");
|
|
63
|
-
|
|
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 (
|
|
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 {
|
|
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:
|
|
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(
|
|
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
|
|
50
|
-
const {
|
|
51
|
-
|
|
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 (
|
|
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
|
}
|
package/dist/app/shapes-page.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 } =
|
|
70
|
-
|
|
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 (
|
|
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
|
|
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 } =
|
|
48
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
}
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { isPresent } from "@manta-eu/lib/present";
|
|
3
|
-
import { Link, Outlet, useLocation
|
|
4
|
-
import { Boxes, Columns3, Database, LayoutGrid, Sigma } from "lucide-react";
|
|
3
|
+
import { Link, Outlet, useLocation } from "react-router";
|
|
4
|
+
import { Boxes, Columns3, Database, LayoutGrid, ListChecks, Sigma } from "lucide-react";
|
|
5
5
|
import { MainContent, MantaLogo, NavUser, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger, } from "@manta-eu/react";
|
|
6
|
-
import { hornsPath, rulesPath, shapesPath, showcasePath } from "../app/paths";
|
|
7
|
-
import {
|
|
6
|
+
import { hornsPath, plansPath, rulesPath, shapesPath, showcasePath } from "../app/paths";
|
|
7
|
+
import { useActiveDatastore } from "@manta-eu/react/providers";
|
|
8
|
+
import { useAuthContext } from "@manta-eu/react/providers/AuthContext";
|
|
8
9
|
function NavLink({ to, icon, label, active }) {
|
|
9
10
|
return (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, isActive: active, tooltip: label, children: _jsxs(Link, { to: to, "aria-current": active ? "page" : undefined, children: [icon, _jsx("span", { children: label })] }) }) }));
|
|
10
11
|
}
|
|
11
12
|
export function AppShell() {
|
|
12
|
-
const { datastoreId } =
|
|
13
|
+
const { datastoreId } = useActiveDatastore();
|
|
13
14
|
const { pathname } = useLocation();
|
|
14
|
-
const { user, datastore, datastores } =
|
|
15
|
+
const { user, datastore, datastores = [] } = useAuthContext();
|
|
15
16
|
const current = datastores.find((candidate) => candidate.id === datastoreId) ?? datastore;
|
|
16
17
|
const inDatastore = isPresent(datastoreId) && isPresent(current);
|
|
17
|
-
return (_jsxs(SidebarProvider, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsxs(Link, { to: "/", className: "flex items-center gap-2 px-2 py-1.5", "aria-label": "Manta Horns", children: [_jsx(MantaLogo, { size: 24 }), _jsx("span", { className: "font-semibold group-data-[collapsible=icon]:hidden", children: "Horns" })] }) }), _jsx(SidebarContent, { children: _jsxs("nav", { "aria-label": "Datastore", className: "flex flex-col gap-2", children: [isPresent(datastoreId) && inDatastore ? (_jsxs(_Fragment, { children: [_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: current.name }), _jsx(SidebarGroupContent, { children: _jsxs(SidebarMenu, { children: [_jsx(NavLink, { to: showcasePath(
|
|
18
|
+
return (_jsxs(SidebarProvider, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsxs(Link, { to: "/", className: "flex items-center gap-2 px-2 py-1.5", "aria-label": "Manta Horns", children: [_jsx(MantaLogo, { size: 24 }), _jsx("span", { className: "font-semibold group-data-[collapsible=icon]:hidden", children: "Horns" })] }) }), _jsx(SidebarContent, { children: _jsxs("nav", { "aria-label": "Datastore", className: "flex flex-col gap-2", children: [isPresent(datastoreId) && inDatastore ? (_jsxs(_Fragment, { children: [_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: current.name }), _jsx(SidebarGroupContent, { children: _jsxs(SidebarMenu, { children: [_jsx(NavLink, { to: showcasePath(), icon: _jsx(LayoutGrid, { "aria-hidden": "true" }), label: "Showcase", active: pathname === showcasePath() }), _jsx(NavLink, { to: hornsPath(), icon: _jsx(Columns3, { "aria-hidden": "true" }), label: "Horns", active: pathname === hornsPath() })] }) })] }), _jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: "For builders" }), _jsx(SidebarGroupContent, { children: _jsxs(SidebarMenu, { children: [_jsx(NavLink, { to: shapesPath(), icon: _jsx(Boxes, { "aria-hidden": "true" }), label: "Data shapes", active: pathname.startsWith(shapesPath()) }), _jsx(NavLink, { to: rulesPath(), icon: _jsx(Sigma, { "aria-hidden": "true" }), label: "Rules", active: pathname.startsWith(rulesPath()) }), _jsx(NavLink, { to: plansPath(), icon: _jsx(ListChecks, { "aria-hidden": "true" }), label: "Action plans", active: pathname.startsWith(plansPath()) })] }) })] })] })) : null, _jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: "All datastores" }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: _jsx(NavLink, { to: "/", icon: _jsx(Database, { "aria-hidden": "true" }), label: `Datastores (${datastores.length})`, active: pathname === "/" }) }) })] })] }) }), _jsx(SidebarFooter, { children: _jsx(NavUser, { name: user?.name ?? user?.email ?? "Signed in", currentDatastore: { name: current?.name ?? "No datastore" }, onNavigateToAccount: () => window.location.assign("/"), onLogout: () => window.location.assign("/logout") }) })] }), _jsx(MainContent, { className: "pb-8", children: _jsxs("div", { className: "flex w-full flex-col gap-4 px-4 sm:px-6", children: [_jsx(SidebarTrigger, { "aria-label": "Toggle Sidebar" }), _jsx(Outlet, {})] }) })] }));
|
|
18
19
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { lazy, Suspense } from "react";
|
|
3
3
|
import { Loader } from "@manta-eu/react";
|
|
4
|
-
import {
|
|
4
|
+
import { EmbeddedChatProviders } from "@manta-eu/react/providers";
|
|
5
5
|
// Behind `lazy`: the body pulls in the graph routing and every typed entity card.
|
|
6
6
|
const CatalogCardBody = lazy(async () => {
|
|
7
7
|
const body = await import("./catalog-card-body");
|
|
8
8
|
return { default: body.CatalogCardBody };
|
|
9
9
|
});
|
|
10
|
-
// The
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
export const catalogCardView = (content, _view, context) => (_jsx(MantaSparqlProvider, { datastoreId: context.datastoreId, children: _jsx(Suspense, { fallback: _jsx(Loader, {}), children: _jsx(CatalogCardBody, { content: content }) }) }));
|
|
10
|
+
// The providers come with it: a card fetches its own entity, and a server-rendered Horn document hydrates
|
|
11
|
+
// embeds and nothing else. The embedded set, because a card must not navigate its host away on a 401.
|
|
12
|
+
export const catalogCardView = (content, _view, context) => (_jsx(EmbeddedChatProviders, { mantaBaseUrl: window.location.origin, datastoreId: context.datastoreId, children: _jsx(Suspense, { fallback: _jsx(Loader, {}), children: _jsx(CatalogCardBody, { content: content }) }) }));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
import { isRecord } from "@manta-eu/horns";
|
|
4
|
+
import { Dialog, DialogContent, DialogTitle } from "@manta-eu/react";
|
|
5
|
+
import { ExplainFact } from "@manta-eu/react/components/explain-fact/ExplainFact";
|
|
6
|
+
// The explanation a `WhyThis` mark asks for. One per page rather than one per mark: the kit draws the marks
|
|
7
|
+
// and the host owns the modal, because drawing a proof tree needs the design system the kit has not got.
|
|
8
|
+
/**
|
|
9
|
+
* Whether what a view handed over is an explanation this can draw.
|
|
10
|
+
*
|
|
11
|
+
* Checked to the depth the type declares — a map of facts, or of lists of them — because the data came out
|
|
12
|
+
* of model-written source and the component walks it without asking.
|
|
13
|
+
*/
|
|
14
|
+
function isExplanation(value) {
|
|
15
|
+
if (!isRecord(value))
|
|
16
|
+
return false;
|
|
17
|
+
return Object.values(value).every((one) => Array.isArray(one) ? one.every((fact) => isRecord(fact)) : isRecord(one));
|
|
18
|
+
}
|
|
19
|
+
export function useExplainDialog() {
|
|
20
|
+
const [shown, setShown] = useState();
|
|
21
|
+
// Stable, because it reaches the kit through a provider value and would otherwise redraw every view.
|
|
22
|
+
const open = useCallback((fact) => setShown(fact), []);
|
|
23
|
+
const explanation = isExplanation(shown?.data) ? shown.data : undefined;
|
|
24
|
+
const dialog = (_jsx(Dialog, { open: shown !== undefined, onOpenChange: (next) => (next ? undefined : setShown(undefined)), children: _jsxs(DialogContent, { children: [_jsx(DialogTitle, { text: shown?.title ?? "Why this value" }), shown === undefined ? null : (_jsx(ExplainFact, { mode: "compact", subject: shown.subject, object: shown.object, ...(shown.predicate === undefined ? {} : { predicate: shown.predicate }), ...(explanation === undefined ? {} : { data: explanation }) }))] }) }));
|
|
25
|
+
return { open, dialog };
|
|
26
|
+
}
|