@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
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
{
|
|
2
|
+
"iri": "urn:manta-horns:horn:reference-supply-chain-360",
|
|
3
|
+
"slug": "supply-chain-360",
|
|
4
|
+
"title": "Supply Chain 360",
|
|
5
|
+
"summary": "Everything known about one counterparty: what is flagged, where it is exposed, who supplies it.",
|
|
6
|
+
"audience": "Supply chain",
|
|
7
|
+
"datastoreId": "salt-house",
|
|
8
|
+
"realm": "hiremanta-com",
|
|
9
|
+
"schemaVersion": 1,
|
|
10
|
+
"revision": 1,
|
|
11
|
+
"layout": {
|
|
12
|
+
"kind": "grid",
|
|
13
|
+
"columns": 12,
|
|
14
|
+
"items": [
|
|
15
|
+
{
|
|
16
|
+
"viewId": "entity",
|
|
17
|
+
"x": 0,
|
|
18
|
+
"y": 0,
|
|
19
|
+
"w": 3,
|
|
20
|
+
"h": 2
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"viewId": "entity-stats",
|
|
24
|
+
"x": 3,
|
|
25
|
+
"y": 0,
|
|
26
|
+
"w": 9,
|
|
27
|
+
"h": 2
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"viewId": "exposure-tiles",
|
|
31
|
+
"x": 0,
|
|
32
|
+
"y": 2,
|
|
33
|
+
"w": 4,
|
|
34
|
+
"h": 4
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"viewId": "global-exposure",
|
|
38
|
+
"x": 4,
|
|
39
|
+
"y": 2,
|
|
40
|
+
"w": 5,
|
|
41
|
+
"h": 4
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"viewId": "notifications",
|
|
45
|
+
"x": 9,
|
|
46
|
+
"y": 2,
|
|
47
|
+
"w": 3,
|
|
48
|
+
"h": 4
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"viewId": "concentration-history",
|
|
52
|
+
"x": 0,
|
|
53
|
+
"y": 6,
|
|
54
|
+
"w": 9,
|
|
55
|
+
"h": 5
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"viewId": "disruptions",
|
|
59
|
+
"x": 9,
|
|
60
|
+
"y": 6,
|
|
61
|
+
"w": 3,
|
|
62
|
+
"h": 5
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"viewId": "top-suppliers",
|
|
66
|
+
"x": 0,
|
|
67
|
+
"y": 11,
|
|
68
|
+
"w": 12,
|
|
69
|
+
"h": 4
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"variables": [
|
|
74
|
+
{
|
|
75
|
+
"id": "entity",
|
|
76
|
+
"label": "Counterparty",
|
|
77
|
+
"type": "iri",
|
|
78
|
+
"urlParam": "entity"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "category",
|
|
82
|
+
"label": "Alert kind",
|
|
83
|
+
"type": "string",
|
|
84
|
+
"urlParam": "kind"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "country",
|
|
88
|
+
"label": "Country",
|
|
89
|
+
"type": "iri",
|
|
90
|
+
"urlParam": "country"
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"views": [
|
|
94
|
+
{
|
|
95
|
+
"id": "entity",
|
|
96
|
+
"title": "Who this is",
|
|
97
|
+
"binding": {
|
|
98
|
+
"kind": "select",
|
|
99
|
+
"sparql": "SELECT ?this ?name ?industry ?country ?entityType WHERE { BIND(?entity AS ?this) OPTIONAL { ?this schema:name ?name } OPTIONAL { ?this :factsetIndustry ?industry } OPTIONAL { ?this schema:addressCountry ?country } OPTIONAL { ?this :entityType ?entityType } }",
|
|
100
|
+
"params": ["entity"],
|
|
101
|
+
"limit": 1,
|
|
102
|
+
"derivations": [],
|
|
103
|
+
"columns": [
|
|
104
|
+
{
|
|
105
|
+
"name": "this",
|
|
106
|
+
"type": "iri"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "name",
|
|
110
|
+
"type": "string"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "industry",
|
|
114
|
+
"type": "string"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "country",
|
|
118
|
+
"type": "iri"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "entityType",
|
|
122
|
+
"type": "string"
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
"render": {
|
|
127
|
+
"kind": "tsx",
|
|
128
|
+
"source": "import { Badge, Button, Separator } from \"@manta-eu/react\";\nimport { useHornFilter } from \"@manta-eu/horns-kit/view\";\n\nconst muted = { opacity: 0.7 };\n\nexport default function View({ rows, labels }) {\n const country = useHornFilter(\"country\");\n const one = rows[0];\n if (!one) return null;\n const named = one.country ? (labels[one.country] ?? \"Unknown country\") : undefined;\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 12 }}>\n <span style={{ fontSize: 20, lineHeight: 1.2, fontWeight: 600 }}>{one.name ?? \"Unnamed entity\"}</span>\n <span style={{ ...muted, fontSize: 14, lineHeight: 1.35 }}>{one.industry}</span>\n <Separator />\n <span style={{ display: \"flex\", flexWrap: \"wrap\", alignItems: \"center\", gap: 8 }}>\n {named && country.live ? (\n <Button variant={country.is(one.country) ? \"primary\" : \"secondary\"} size=\"xs\" onClick={() => country.toggle(one.country)}>\n {named}\n </Button>\n ) : named ? (\n <Badge variant=\"secondary\">{named}</Badge>\n ) : null}\n {one.entityType ? <Badge variant=\"secondary\">{one.entityType}</Badge> : null}\n </span>\n </div>\n );\n}",
|
|
129
|
+
"reviewed": {
|
|
130
|
+
"hash": "sha256-tfqcvzciL4h4fl2Z7k58XJBW7uzMCdwTkzj1OHa7qzM",
|
|
131
|
+
"at": "2026-09-29T12:00:00.000Z"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "notifications",
|
|
137
|
+
"title": "Supply chain notifications",
|
|
138
|
+
"binding": {
|
|
139
|
+
"kind": "select",
|
|
140
|
+
"sparql": "SELECT ?this ?headline ?source ?priority ?raised ?category WHERE { ?this :concerns ?entity ; :alertKind \"notification\" ; :headline ?headline ; :alertCategory ?category . OPTIONAL { ?this :alertSource ?source } OPTIONAL { ?this :alertPriority ?priority } OPTIONAL { ?this :raisedAt ?raised } } ORDER BY DESC(?raised)",
|
|
141
|
+
"params": ["entity", "category"],
|
|
142
|
+
"limit": 50,
|
|
143
|
+
"derivations": [],
|
|
144
|
+
"columns": [
|
|
145
|
+
{
|
|
146
|
+
"name": "this",
|
|
147
|
+
"type": "iri"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"name": "headline",
|
|
151
|
+
"type": "string"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"name": "source",
|
|
155
|
+
"type": "string"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "priority",
|
|
159
|
+
"type": "string"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"name": "raised",
|
|
163
|
+
"type": "date"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"name": "category",
|
|
167
|
+
"type": "string"
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
"render": {
|
|
172
|
+
"kind": "tsx",
|
|
173
|
+
"source": "import { Badge, Button, Link, ScrollArea, Separator, Status } from \"@manta-eu/react\";\nimport { Nothing, useHornFilter } from \"@manta-eu/horns-kit/view\";\n\nconst muted = { opacity: 0.7 };\n\n// Every row says where it came from and what it is worth. That is the property worth copying from Supply\n// Chain 360: an alert with no provenance is a rumour. The words are a person's, not the graph's.\n//\n// Where a row goes is `links`, which the host composes — never an href built from a column. A URL in the\n// data is a URL out of the app, and what a link should open is not the view's to decide.\nconst said = { red: \"Urgent\", amber: \"Needs attention\", green: \"For information\" };\nconst tone = (priority) =>\n priority === \"red\" ? \"error\" : priority === \"amber\" ? \"warning\" : priority === \"green\" ? \"success\" : \"neutral\";\n\nexport default function View({ rows, links }) {\n const category = useHornFilter(\"category\");\n const narrowed = typeof category.value === \"string\" ? category.value : undefined;\n return (\n <div style={{ display: \"flex\", height: \"100%\", minHeight: 0, flexDirection: \"column\", gap: 12 }}>\n {narrowed ? (\n <span style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", gap: 8 }}>\n <span style={{ ...muted, fontSize: 12 }}>Only {narrowed}</span>\n <Button variant=\"ghost\" size=\"xs\" onClick={() => category.toggle(narrowed)}>\n Show all\n </Button>\n </span>\n ) : null}\n {rows.length === 0 ? (\n <Nothing title=\"Nothing raised\" hint=\"Rules raise these as they find work.\" />\n ) : (\n <ScrollArea style={{ minHeight: 0, flex: 1 }}>\n <ul style={{ display: \"flex\", flexDirection: \"column\", gap: 12 }}>\n {rows.map((row, index) => (\n <li key={row.this} style={{ display: \"flex\", flexDirection: \"column\", gap: 8 }}>\n {index === 0 ? null : <Separator />}\n <span style={{ fontSize: 14, lineHeight: 1.35 }}>{row.headline}</span>\n <span style={{ display: \"flex\", flexWrap: \"wrap\", alignItems: \"center\", gap: 8 }}>\n {row.priority ? (\n <Status label={said[row.priority] ?? row.priority} variant={tone(row.priority)} />\n ) : null}\n {row.source ? <Badge variant=\"secondary\">{row.source}</Badge> : null}\n {links[row.this] ? (\n <Link href={links[row.this].href} variant=\"ghost\" size=\"xs\">\n {links[row.this].label}\n </Link>\n ) : null}\n </span>\n </li>\n ))}\n </ul>\n </ScrollArea>\n )}\n </div>\n );\n}",
|
|
174
|
+
"reviewed": {
|
|
175
|
+
"hash": "sha256-XA8o7l3yR7VosLu3uqbXQk3cOWKUJUP0TcQzi15v9Sc",
|
|
176
|
+
"at": "2026-09-29T12:00:00.000Z"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"id": "disruptions",
|
|
182
|
+
"title": "Disruptions",
|
|
183
|
+
"binding": {
|
|
184
|
+
"kind": "select",
|
|
185
|
+
"sparql": "SELECT ?this ?headline ?source ?priority ?raised ?category WHERE { ?this :concerns ?entity ; :alertKind \"disruption\" ; :headline ?headline ; :alertCategory ?category . OPTIONAL { ?this :alertSource ?source } OPTIONAL { ?this :alertPriority ?priority } OPTIONAL { ?this :raisedAt ?raised } } ORDER BY DESC(?raised)",
|
|
186
|
+
"params": ["entity", "category"],
|
|
187
|
+
"limit": 50,
|
|
188
|
+
"derivations": [],
|
|
189
|
+
"columns": [
|
|
190
|
+
{
|
|
191
|
+
"name": "this",
|
|
192
|
+
"type": "iri"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"name": "headline",
|
|
196
|
+
"type": "string"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"name": "source",
|
|
200
|
+
"type": "string"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"name": "priority",
|
|
204
|
+
"type": "string"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"name": "raised",
|
|
208
|
+
"type": "date"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "category",
|
|
212
|
+
"type": "string"
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
},
|
|
216
|
+
"render": {
|
|
217
|
+
"kind": "tsx",
|
|
218
|
+
"source": "import { Badge, Button, Link, ScrollArea, Separator, Status } from \"@manta-eu/react\";\nimport { Nothing, useHornFilter } from \"@manta-eu/horns-kit/view\";\n\nconst muted = { opacity: 0.7 };\n\n// Every row says where it came from and what it is worth. That is the property worth copying from Supply\n// Chain 360: an alert with no provenance is a rumour. The words are a person's, not the graph's.\n//\n// Where a row goes is `links`, which the host composes — never an href built from a column. A URL in the\n// data is a URL out of the app, and what a link should open is not the view's to decide.\nconst said = { red: \"Urgent\", amber: \"Needs attention\", green: \"For information\" };\nconst tone = (priority) =>\n priority === \"red\" ? \"error\" : priority === \"amber\" ? \"warning\" : priority === \"green\" ? \"success\" : \"neutral\";\n\nexport default function View({ rows, links }) {\n const category = useHornFilter(\"category\");\n const narrowed = typeof category.value === \"string\" ? category.value : undefined;\n return (\n <div style={{ display: \"flex\", height: \"100%\", minHeight: 0, flexDirection: \"column\", gap: 12 }}>\n {narrowed ? (\n <span style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", gap: 8 }}>\n <span style={{ ...muted, fontSize: 12 }}>Only {narrowed}</span>\n <Button variant=\"ghost\" size=\"xs\" onClick={() => category.toggle(narrowed)}>\n Show all\n </Button>\n </span>\n ) : null}\n {rows.length === 0 ? (\n <Nothing title=\"Nothing raised\" hint=\"Events affecting this entity appear here.\" />\n ) : (\n <ScrollArea style={{ minHeight: 0, flex: 1 }}>\n <ul style={{ display: \"flex\", flexDirection: \"column\", gap: 12 }}>\n {rows.map((row, index) => (\n <li key={row.this} style={{ display: \"flex\", flexDirection: \"column\", gap: 8 }}>\n {index === 0 ? null : <Separator />}\n <span style={{ fontSize: 14, lineHeight: 1.35 }}>{row.headline}</span>\n <span style={{ display: \"flex\", flexWrap: \"wrap\", alignItems: \"center\", gap: 8 }}>\n {row.priority ? (\n <Status label={said[row.priority] ?? row.priority} variant={tone(row.priority)} />\n ) : null}\n {row.source ? <Badge variant=\"secondary\">{row.source}</Badge> : null}\n {links[row.this] ? (\n <Link href={links[row.this].href} variant=\"ghost\" size=\"xs\">\n {links[row.this].label}\n </Link>\n ) : null}\n </span>\n </li>\n ))}\n </ul>\n </ScrollArea>\n )}\n </div>\n );\n}",
|
|
219
|
+
"reviewed": {
|
|
220
|
+
"hash": "sha256-Vg6rer0ebCYI9tXTrGxWClmK8Wvr39W2MN0XSCE3wOA",
|
|
221
|
+
"at": "2026-09-29T12:00:00.000Z"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"id": "entity-stats",
|
|
227
|
+
"title": "Entity stats",
|
|
228
|
+
"binding": {
|
|
229
|
+
"kind": "select",
|
|
230
|
+
"sparql": "SELECT (COUNT(DISTINCT ?order) AS ?outgoing) (SUM(?amount) AS ?outgoingValue) (COUNT(DISTINCT ?supplier) AS ?suppliers) (COUNT(DISTINCT ?customer) AS ?customers) WHERE { OPTIONAL { ?entity :placedOrder ?order . OPTIONAL { ?order schema:totalPrice ?amount } } OPTIONAL { ?entity :tier1Supplier ?supplier } OPTIONAL { ?entity :tier1Customer ?customer } }",
|
|
231
|
+
"params": ["entity"],
|
|
232
|
+
"limit": 1,
|
|
233
|
+
"derivations": [],
|
|
234
|
+
"columns": [
|
|
235
|
+
{
|
|
236
|
+
"name": "outgoing",
|
|
237
|
+
"type": "number"
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"name": "outgoingValue",
|
|
241
|
+
"type": "number"
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"name": "suppliers",
|
|
245
|
+
"type": "number"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"name": "customers",
|
|
249
|
+
"type": "number"
|
|
250
|
+
}
|
|
251
|
+
]
|
|
252
|
+
},
|
|
253
|
+
"render": {
|
|
254
|
+
"kind": "tsx",
|
|
255
|
+
"source": "import { Separator } from \"@manta-eu/react\";\nimport { WhyThis } from \"@manta-eu/horns-kit/view\";\n\nconst muted = { opacity: 0.7 };\n\nconst money = (value) =>\n typeof value === \"number\"\n ? `$${value.toLocaleString(\"en-US\", { notation: \"compact\", maximumFractionDigits: 2 })}`\n : \"—\";\nconst count = (value) => (typeof value === \"number\" ? value.toLocaleString(\"en-US\") : \"—\");\n\n/** Each figure is counted, not recorded, so each one can say what it counted. */\nfunction countedFrom(subject, predicate, over, value) {\n const claim = `${predicate}[<${subject}>, ${value}]`;\n return {\n [claim]: {\n fact: claim,\n type: \"derived\",\n \"rule-instances-complete\": true,\n \"rule-instances\": [\n {\n rule: `${predicate}[?entity, ?total] :- AGGREGATE(${over}[?entity, ?one] ON ?entity BIND COUNT(?one) AS ?total) .`,\n \"grounded-rule\": `${claim} :- AGGREGATE(${over}[<${subject}>, ?one] ON <${subject}> BIND COUNT(?one) AS ${value}) .`,\n \"body-facts\": [],\n },\n ],\n },\n };\n}\n\nexport default function View({ rows }) {\n const one = rows[0] ?? {};\n const subject = one.this ?? \"\";\n const stats = [\n { id: \":outgoingTransactions\", over: \":placedOrder\", raw: one.outgoing, value: count(one.outgoing), label: \"Total outgoing transactions\" },\n { id: \":outgoingValue\", over: \":placedOrder\", raw: one.outgoingValue, value: money(one.outgoingValue), label: \"Total outgoing transaction value\" },\n { id: \":tier1SupplierCount\", over: \":tier1Supplier\", raw: one.suppliers, value: count(one.suppliers), label: \"Tier 1 suppliers\" },\n { id: \":tier1CustomerCount\", over: \":tier1Customer\", raw: one.customers, value: count(one.customers), label: \"Tier 1 customers\" },\n ];\n return (\n <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(4, minmax(0, 1fr))\", alignItems: \"center\" }}>\n {stats.map((stat, index) => (\n <span key={stat.label} style={{ display: \"flex\", minWidth: 0, alignItems: \"center\", gap: 20 }}>\n {index === 0 ? null : <Separator orientation=\"vertical\" style={{ height: 40 }} />}\n <span style={{ display: \"flex\", minWidth: 0, flexDirection: \"column\", gap: 4 }}>\n <span style={{ fontSize: 20, lineHeight: 1, fontWeight: 600, fontVariantNumeric: \"tabular-nums\" }}>\n <WhyThis\n subject={subject}\n predicate={stat.id}\n object={String(stat.raw ?? \"\")}\n title={stat.label}\n data={countedFrom(subject, stat.id, stat.over, stat.raw ?? 0)}\n >\n {stat.value}\n </WhyThis>\n </span>\n <span style={{ ...muted, fontSize: 12, lineHeight: 1.35 }}>{stat.label}</span>\n </span>\n </span>\n ))}\n </div>\n );\n}",
|
|
256
|
+
"reviewed": {
|
|
257
|
+
"hash": "sha256-pS3MIel4V5h24O74GydrKs2E8DQDx_YaBvKMU_ImE48",
|
|
258
|
+
"at": "2026-09-29T12:00:00.000Z"
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"id": "exposure-tiles",
|
|
264
|
+
"title": "What is flagged",
|
|
265
|
+
"binding": {
|
|
266
|
+
"kind": "select",
|
|
267
|
+
"sparql": "SELECT ?kind (COUNT(DISTINCT ?alert) AS ?total) (SAMPLE(?note) AS ?qualifier) (SAMPLE(?level) AS ?severity) WHERE { ?alert :concerns ?entity ; :alertKind ?kind . OPTIONAL { ?alert :qualifier ?note } OPTIONAL { ?alert :severity ?level } } GROUP BY ?kind",
|
|
268
|
+
"params": ["entity"],
|
|
269
|
+
"limit": 12,
|
|
270
|
+
"derivations": [],
|
|
271
|
+
"columns": [
|
|
272
|
+
{
|
|
273
|
+
"name": "kind",
|
|
274
|
+
"type": "string"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"name": "total",
|
|
278
|
+
"type": "number"
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"name": "qualifier",
|
|
282
|
+
"type": "string"
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"name": "severity",
|
|
286
|
+
"type": "string"
|
|
287
|
+
}
|
|
288
|
+
]
|
|
289
|
+
},
|
|
290
|
+
"render": {
|
|
291
|
+
"kind": "tsx",
|
|
292
|
+
"source": "import { Button, Card, CardContent, Status } from \"@manta-eu/react\";\nimport { Nothing, useHornFilter } from \"@manta-eu/horns-kit/view\";\n\n// Each tile is a filter on the feed beside it, which is the property worth copying: a number you cannot\n// press is a statistic, and a number you can is a way in. The tile and the feed are not wired to each\n// other — they both answer to the Horn's `category`.\nconst tone = (severity) => (severity === \"high\" ? \"error\" : severity === \"medium\" ? \"warning\" : \"neutral\");\n\nexport default function View({ rows }) {\n const category = useHornFilter(\"category\");\n if (rows.length === 0) {\n return <Nothing title=\"Nothing flagged\" hint=\"Alerts about this entity appear here as rules find them.\" />;\n }\n return (\n <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(2, minmax(0, 1fr))\", gap: 12 }}>\n {rows.map((row) => (\n <Card\n key={row.kind}\n data-selected={category.is(row.kind) ? \"true\" : undefined}\n style={{ outline: category.is(row.kind) ? \"2px solid currentColor\" : undefined }}\n >\n <CardContent style={{ padding: 0 }}>\n <Button\n variant=\"ghost\"\n disabled={!category.live}\n onClick={() => category.toggle(row.kind)}\n style={{ height: \"auto\", width: \"100%\", flexDirection: \"column\", alignItems: \"flex-start\", gap: 8, padding: \"12px 16px\", textAlign: \"left\" }}\n >\n <span style={{ display: \"flex\", alignItems: \"baseline\", gap: 8 }}>\n <span style={{ fontSize: 24, lineHeight: 1, fontWeight: 600, fontVariantNumeric: \"tabular-nums\" }}>{row.total ?? 0}</span>\n <span style={{ fontSize: 14, lineHeight: 1.35 }}>{row.kind}</span>\n </span>\n {row.qualifier ? <Status label={row.qualifier} variant={tone(row.severity)} /> : null}\n </Button>\n </CardContent>\n </Card>\n ))}\n </div>\n );\n}",
|
|
293
|
+
"reviewed": {
|
|
294
|
+
"hash": "sha256-aq1E3_aTrfvoWTMfy3XniJEkX-cSnn7YCOxqZaotqg4",
|
|
295
|
+
"at": "2026-09-29T12:00:00.000Z"
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"id": "global-exposure",
|
|
301
|
+
"title": "Global exposure",
|
|
302
|
+
"binding": {
|
|
303
|
+
"kind": "select",
|
|
304
|
+
"sparql": "SELECT ?this ?buyerConcentration ?supplierConcentration ?disruptions WHERE { ?entity :exposedTo ?exposure . ?exposure schema:addressCountry ?this . OPTIONAL { ?exposure :buyerConcentration ?buyerConcentration } OPTIONAL { ?exposure :supplierConcentration ?supplierConcentration } OPTIONAL { ?exposure :activeDisruptions ?disruptions } } ORDER BY DESC(?supplierConcentration)",
|
|
305
|
+
"params": ["entity"],
|
|
306
|
+
"limit": 40,
|
|
307
|
+
"derivations": [],
|
|
308
|
+
"columns": [
|
|
309
|
+
{
|
|
310
|
+
"name": "this",
|
|
311
|
+
"type": "iri"
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"name": "buyerConcentration",
|
|
315
|
+
"type": "number"
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"name": "supplierConcentration",
|
|
319
|
+
"type": "number"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"name": "disruptions",
|
|
323
|
+
"type": "number"
|
|
324
|
+
}
|
|
325
|
+
]
|
|
326
|
+
},
|
|
327
|
+
"render": {
|
|
328
|
+
"kind": "tsx",
|
|
329
|
+
"source": "import { Button, Status } from \"@manta-eu/react\";\nimport { Nothing, useHornFilter } from \"@manta-eu/horns-kit/view\";\n\nconst muted = { opacity: 0.7 };\n\n// The gap, drawn honestly. Supply Chain 360 puts two scales on one choropleth with per-layer toggles and\n// disruption markers over both; nothing on the view surface draws a map, so the same numbers are ranked —\n// and pressing a country narrows the suppliers beneath it, which the map also does.\nfunction Meter({ label, value }) {\n return (\n <span style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n <span style={{ ...muted, width: 56, flexShrink: 0, fontSize: 12 }}>{label}</span>\n <span style={{ position: \"relative\", height: 6, minWidth: 0, flex: 1, overflow: \"hidden\" }}>\n <span style={{ position: \"absolute\", inset: 0, background: \"currentColor\", opacity: 0.15 }} />\n <span\n style={{\n position: \"absolute\",\n left: 0,\n top: 0,\n bottom: 0,\n background: \"currentColor\",\n width: `${Math.min(100, Math.max(0, value))}%`,\n }}\n />\n </span>\n <span style={{ width: 28, flexShrink: 0, textAlign: \"right\", fontSize: 12, fontVariantNumeric: \"tabular-nums\" }}>{Math.round(value)}</span>\n </span>\n );\n}\n\nexport default function View({ rows, labels }) {\n const country = useHornFilter(\"country\");\n if (rows.length === 0) {\n return <Nothing title=\"No exposure recorded\" hint=\"Countries appear here once transactions name them.\" />;\n }\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 4 }}>\n {rows.map((row) => (\n <Button\n key={row.this}\n variant=\"ghost\"\n disabled={!country.live}\n onClick={() => country.toggle(row.this)}\n style={{\n height: \"auto\",\n width: \"100%\",\n flexDirection: \"column\",\n alignItems: \"stretch\",\n gap: 6,\n padding: \"6px 8px\",\n outline: country.is(row.this) ? \"2px solid currentColor\" : undefined,\n }}\n >\n <span style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", gap: 8 }}>\n <span style={{ fontSize: 14 }}>{labels[row.this] ?? \"Unknown country\"}</span>\n {row.disruptions ? (\n <Status\n label={`${row.disruptions} ${row.disruptions === 1 ? \"disruption\" : \"disruptions\"}`}\n variant=\"warning\"\n />\n ) : null}\n </span>\n <Meter label=\"Supplier\" value={row.supplierConcentration ?? 0} />\n <Meter label=\"Buyer\" value={row.buyerConcentration ?? 0} />\n </Button>\n ))}\n </div>\n );\n}",
|
|
330
|
+
"reviewed": {
|
|
331
|
+
"hash": "sha256-sElmawlSY1Z1RnHzK8Efe_4fTf-evXnZYoO08fMfZsA",
|
|
332
|
+
"at": "2026-09-29T12:00:00.000Z"
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"id": "concentration-history",
|
|
338
|
+
"title": "Concentration history",
|
|
339
|
+
"binding": {
|
|
340
|
+
"kind": "select",
|
|
341
|
+
"sparql": "SELECT ?month (AVG(?score) AS ?concentration) WHERE { ?entity :tier1Supplier ?supplier . ?supplier :concentrationReading ?reading . ?reading :readingMonth ?month ; :concentrationScore ?score } GROUP BY ?month ORDER BY ?month",
|
|
342
|
+
"params": ["entity"],
|
|
343
|
+
"limit": 60,
|
|
344
|
+
"derivations": [],
|
|
345
|
+
"columns": [
|
|
346
|
+
{
|
|
347
|
+
"name": "month",
|
|
348
|
+
"type": "string"
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"name": "concentration",
|
|
352
|
+
"type": "number"
|
|
353
|
+
}
|
|
354
|
+
]
|
|
355
|
+
},
|
|
356
|
+
"render": {
|
|
357
|
+
"kind": "tsx",
|
|
358
|
+
"source": "import { AreaChart } from \"@manta-eu/react/chart\";\nimport { Nothing } from \"@manta-eu/horns-kit/view\";\n\nconst muted = { opacity: 0.7 };\n\n// The annotations are the half already paid for: the SDK accepts them and the view surface did not list\n// them, so the Studio had never written one. The band and the benchmark are what let the picture answer\n// \"is this bad\" without a legend.\nexport default function View({ rows }) {\n if (rows.length === 0) {\n return <Nothing title=\"No readings yet\" hint=\"Concentration is scored monthly once orders are loaded.\" />;\n }\n return (\n <div style={{ display: \"flex\", height: \"100%\", minHeight: 0, flexDirection: \"column\", gap: 8 }}>\n <span style={{ ...muted, fontSize: 14 }}>\n Higher means more of this entity's supply sits with fewer suppliers.\n </span>\n <span style={{ minHeight: 0, flex: 1 }}>\n <AreaChart\n data={{\n type: \"area\",\n data: rows.map((row) => ({ x: row.month, y: row.concentration ?? 0 })),\n yAxisLabel: \"Concentration score\",\n xAxisLabel: \"Month\",\n annotations: [\n { kind: \"reference-area\", axis: \"y\", from: 70, to: 100, label: \"Concentrated\", tone: \"danger\" },\n { kind: \"reference-line\", axis: \"y\", value: 70, label: \"Benchmark 4\", tone: \"warning\" },\n { kind: \"stat-line\", stat: \"average\", axis: \"y\", label: \"Average\", tone: \"neutral\" },\n ],\n }}\n />\n </span>\n </div>\n );\n}",
|
|
359
|
+
"reviewed": {
|
|
360
|
+
"hash": "sha256-APKf-WP6MesibH3o6eyDfbnHgaPzunvv9ACy5Dc6Qpw",
|
|
361
|
+
"at": "2026-09-29T12:00:00.000Z"
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"id": "top-suppliers",
|
|
367
|
+
"title": "Top suppliers",
|
|
368
|
+
"binding": {
|
|
369
|
+
"kind": "select",
|
|
370
|
+
"sparql": "SELECT ?this ?concentration ?yearOnYear ?country WHERE { ?entity :tier1Supplier ?this . ?this schema:addressCountry ?country . OPTIONAL { ?this :ltmConcentration ?concentration } OPTIONAL { ?this :concentrationYoY ?yearOnYear } } ORDER BY DESC(?concentration)",
|
|
371
|
+
"params": ["entity", "country"],
|
|
372
|
+
"limit": 100,
|
|
373
|
+
"derivations": [],
|
|
374
|
+
"columns": [
|
|
375
|
+
{
|
|
376
|
+
"name": "this",
|
|
377
|
+
"type": "iri"
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
"name": "concentration",
|
|
381
|
+
"type": "number"
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"name": "yearOnYear",
|
|
385
|
+
"type": "number"
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
"name": "country",
|
|
389
|
+
"type": "iri"
|
|
390
|
+
}
|
|
391
|
+
]
|
|
392
|
+
},
|
|
393
|
+
"render": {
|
|
394
|
+
"kind": "tsx",
|
|
395
|
+
"source": "import {\n Badge,\n Button,\n Link,\n ScrollArea,\n Status,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@manta-eu/react\";\nimport { Nothing, WhyThis, useHornFilter } from \"@manta-eu/horns-kit/view\";\n\n// The picture above and the numbers here say the same thing; neither does the other's job. The judgement is\n// already made in the cell, and the cell says why it was made.\nconst tone = (value) => (value >= 50 ? \"error\" : value >= 10 ? \"warning\" : \"success\");\nconst signed = (value) => (typeof value === \"number\" ? `${value > 0 ? \"+\" : \"\"}${value.toFixed(2)}` : \"—\");\n\nconst concentrated = \":concentrated\";\nconst ltm = \":ltmConcentration\";\n\n/** The derivation behind one supplier's score, in the shape RDFox answers an explain request with. */\nfunction explanationOf(supplier, score) {\n const claim = `${concentrated}[<${supplier}>, true]`;\n const evidence = `${ltm}[<${supplier}>, ${score}]`;\n return {\n [claim]: {\n fact: claim,\n type: \"derived\",\n \"rule-instances-complete\": true,\n \"rule-instances\": [\n {\n rule: `${concentrated}[?supplier, true] :- ${ltm}[?supplier, ?score], FILTER(?score > 50) .`,\n \"grounded-rule\": `${claim} :- ${evidence}, FILTER(${score} > 50) .`,\n \"body-facts\": [evidence],\n },\n ],\n },\n [evidence]: { fact: evidence, type: \"explicit\" },\n };\n}\n\nexport default function View({ rows, labels, links }) {\n const country = useHornFilter(\"country\");\n if (rows.length === 0) {\n return (\n <Nothing\n title=\"No suppliers here\"\n hint={country.live ? \"Nothing matches the country this is narrowed to.\" : \"Suppliers appear once orders name them.\"}\n />\n );\n }\n return (\n <ScrollArea style={{ height: \"100%\" }}>\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Supplier</TableHead>\n <TableHead>LTM concentration</TableHead>\n <TableHead style={{ textAlign: \"right\" }}>YoY change</TableHead>\n <TableHead>Country</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {rows.map((row) => (\n <TableRow key={row.this}>\n <TableCell>\n {links[row.this] ? (\n <Link href={links[row.this].href} variant=\"ghost\" size=\"xs\">\n {labels[row.this] ?? \"Unnamed supplier\"}\n </Link>\n ) : (\n (labels[row.this] ?? \"Unnamed supplier\")\n )}\n </TableCell>\n <TableCell>\n <WhyThis\n subject={row.this}\n predicate={concentrated}\n object=\"true\"\n title=\"Why this counts as concentrated\"\n data={explanationOf(row.this, row.concentration ?? 0)}\n >\n <Status label={(row.concentration ?? 0).toFixed(2)} variant={tone(row.concentration ?? 0)} />\n </WhyThis>\n </TableCell>\n <TableCell style={{ textAlign: \"right\", fontVariantNumeric: \"tabular-nums\" }}>{signed(row.yearOnYear)}</TableCell>\n <TableCell>\n {row.country && country.live ? (\n <Button\n variant={country.is(row.country) ? \"primary\" : \"secondary\"}\n size=\"xs\"\n onClick={() => country.toggle(row.country)}\n >\n {labels[row.country] ?? \"Unknown\"}\n </Button>\n ) : row.country ? (\n <Badge variant=\"secondary\">{labels[row.country] ?? \"Unknown\"}</Badge>\n ) : null}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </ScrollArea>\n );\n}",
|
|
396
|
+
"reviewed": {
|
|
397
|
+
"hash": "sha256-LKFpSDObwpNFQVsaQtPC0c5KFFZODUM3OecZv68wTis",
|
|
398
|
+
"at": "2026-09-29T12:00:00.000Z"
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
],
|
|
403
|
+
"derivations": [],
|
|
404
|
+
"actions": [],
|
|
405
|
+
"dependencies": [],
|
|
406
|
+
"lock": {
|
|
407
|
+
"lockVersion": 1,
|
|
408
|
+
"entries": []
|
|
409
|
+
},
|
|
410
|
+
"runtime": {
|
|
411
|
+
"kit": "0.0.0"
|
|
412
|
+
}
|
|
413
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useAuth } from "@manta-eu/react/hooks/useAuth";
|
|
3
|
+
import { AuthContext } from "@manta-eu/react/providers/AuthContext";
|
|
4
|
+
import { useActiveDatastore } from "@manta-eu/react/providers";
|
|
5
|
+
import { isPresent } from "@manta-eu/lib/present";
|
|
6
|
+
import { signedInParam } from "@manta-eu/horns-server/shared";
|
|
7
|
+
import { AppRoutes } from "../app/routes";
|
|
8
|
+
import { Centered, SignIn, SignInLooped } from "./gates";
|
|
9
|
+
import { DatastoreMissing } from "../components/states";
|
|
10
|
+
import { useCustomerDatastoresQuery } from "@manta-eu/client";
|
|
11
|
+
import { useMantaClient } from "@manta-eu/react/providers";
|
|
12
|
+
// Horns' `/login` stamps this, so a return carrying it with no session is a round trip that did not
|
|
13
|
+
// complete rather than a visitor who has not tried. `useAuth` has no equivalent and its generic path loops.
|
|
14
|
+
const justSignedIn = () => new URLSearchParams(window.location.search).get(signedInParam) === "1";
|
|
15
|
+
// Keycloak hands a failure back on the URL. `useAuth` cannot see it, and without this the page offers
|
|
16
|
+
// "sign in" again to someone whose sign-in just failed.
|
|
17
|
+
const authError = () => new URLSearchParams(window.location.search).get("auth_error") ?? undefined;
|
|
18
|
+
/** The session gate, for a Horns that is the whole page. A host that has already gated mounts none of it. */
|
|
19
|
+
export function HornsApp() {
|
|
20
|
+
const auth = useAuth();
|
|
21
|
+
const { datastoreId } = useActiveDatastore();
|
|
22
|
+
const { mantaClient } = useMantaClient();
|
|
23
|
+
// `sessionContext` lists the datastores a member belongs to, which for an operator is none.
|
|
24
|
+
const customers = useCustomerDatastoresQuery(mantaClient, undefined, { enabled: auth.isOperator });
|
|
25
|
+
const datastores = auth.isOperator ? (customers.data?.customerDatastores ?? []) : auth.datastores;
|
|
26
|
+
const failed = authError();
|
|
27
|
+
if (isPresent(failed))
|
|
28
|
+
return _jsx(Centered, { role: "alert", children: failed });
|
|
29
|
+
if (auth.isLoading)
|
|
30
|
+
return _jsx(Centered, { children: "Loading\u2026" });
|
|
31
|
+
if (isPresent(auth.error))
|
|
32
|
+
return _jsx(Centered, { role: "alert", children: auth.error });
|
|
33
|
+
if (!auth.isSessionValid)
|
|
34
|
+
return justSignedIn() ? _jsx(SignInLooped, {}) : _jsx(SignIn, {});
|
|
35
|
+
if (auth.contextFailed)
|
|
36
|
+
return _jsx(Centered, { role: "alert", children: "Could not load your account. Try again." });
|
|
37
|
+
if (!isPresent(auth.user))
|
|
38
|
+
return _jsx(Centered, { children: "Loading\u2026" });
|
|
39
|
+
// Refused here rather than left to a page to render empty, in the words every page already used.
|
|
40
|
+
if (isPresent(datastoreId) && !auth.datastoreAccessGranted) {
|
|
41
|
+
return (_jsx("main", { className: "grid min-h-screen place-items-center p-8", children: _jsx(DatastoreMissing, {}) }));
|
|
42
|
+
}
|
|
43
|
+
return (_jsx(AuthContext.Provider, { value: {
|
|
44
|
+
user: auth.user,
|
|
45
|
+
datastore: auth.datastore,
|
|
46
|
+
datastores,
|
|
47
|
+
logout: auth.logout,
|
|
48
|
+
isOperator: auth.isOperator,
|
|
49
|
+
canManageTeam: auth.canManageTeam,
|
|
50
|
+
canManageIntegrations: auth.canManageIntegrations,
|
|
51
|
+
canManageStreams: auth.canManageStreams,
|
|
52
|
+
canManageSharedMemories: auth.canManageSharedMemories,
|
|
53
|
+
canSwitchDatastore: auth.canSwitchDatastore,
|
|
54
|
+
allowedModels: auth.allowedModels,
|
|
55
|
+
orgRoles: auth.orgRoles,
|
|
56
|
+
}, children: _jsx(AppRoutes, {}) }));
|
|
57
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ShapeCatalogSource } from "../lib/shapes/catalog";
|
|
2
|
+
import "../app/globals.css";
|
|
3
|
+
export type MountHornsOptions = {
|
|
4
|
+
/** The realm's shapes. Horns ships none: they describe a deployment's data model, not the library's. */
|
|
5
|
+
shapes: ShapeCatalogSource;
|
|
6
|
+
/** Defaults to `#app`, which is what this package's own document declares. */
|
|
7
|
+
container?: HTMLElement;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Mount Horns as a whole page. A host with a router of its own splices `hornsRoutes()` in instead.
|
|
11
|
+
*
|
|
12
|
+
* The base URL is this deployment's origin, never agents': the browser holds this worker's session cookies
|
|
13
|
+
* and none of agents', so every SDK call has to land here to be proxied with them.
|
|
14
|
+
*/
|
|
15
|
+
export declare function mountHorns({ shapes, container }: MountHornsOptions): void;
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { StrictMode } from "react";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
|
-
import { BrowserRouter } from "react-router";
|
|
5
4
|
import { ErrorBoundary } from "@sentry/react";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import "
|
|
5
|
+
import { Providers } from "@manta-eu/react/providers";
|
|
6
|
+
import { provideShapeCatalog } from "../lib/shapes/catalog";
|
|
7
|
+
import { initSentry } from "../lib/telemetry/sentry";
|
|
8
|
+
import { HornsApp } from "./app";
|
|
9
|
+
import "../app/globals.css";
|
|
11
10
|
function AppCrashed() {
|
|
12
11
|
return (_jsx("main", { className: "grid min-h-screen place-items-center p-8 text-center", children: _jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: "Something went wrong" }), _jsx("p", { className: "mt-2 text-sm opacity-70", children: "Reload the page to try again." })] }) }));
|
|
13
12
|
}
|
|
14
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Mount Horns as a whole page. A host with a router of its own splices `hornsRoutes()` in instead.
|
|
15
|
+
*
|
|
16
|
+
* The base URL is this deployment's origin, never agents': the browser holds this worker's session cookies
|
|
17
|
+
* and none of agents', so every SDK call has to land here to be proxied with them.
|
|
18
|
+
*/
|
|
15
19
|
export function mountHorns({ shapes, container }) {
|
|
16
20
|
provideShapeCatalog(shapes);
|
|
17
21
|
initSentry();
|
|
18
22
|
const root = container ?? document.getElementById("app");
|
|
19
23
|
if (!root)
|
|
20
24
|
throw new Error("mountHorns found no container: pass one, or give the document an #app element.");
|
|
21
|
-
createRoot(root).render(_jsx(StrictMode, { children: _jsx(ErrorBoundary, { fallback: _jsx(AppCrashed, {}), children: _jsx(
|
|
25
|
+
createRoot(root).render(_jsx(StrictMode, { children: _jsx(ErrorBoundary, { fallback: _jsx(AppCrashed, {}), children: _jsx(Providers, { mantaBaseUrl: window.location.origin, children: _jsx(HornsApp, {}) }) }) }));
|
|
22
26
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
type CenteredProps = {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
role?: "alert";
|
|
5
|
+
};
|
|
6
|
+
export declare function Centered({ children, role }: CenteredProps): import("react").JSX.Element;
|
|
7
|
+
export declare function SignIn(): import("react").JSX.Element;
|
|
8
|
+
/** A return from Keycloak carrying the marker but no session: the round trip did not complete. */
|
|
9
|
+
export declare function SignInLooped(): import("react").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { signInHref } from "../lib/manta/sign-in";
|
|
3
|
+
export function Centered({ children, role }) {
|
|
4
|
+
return (_jsx("main", { className: "grid min-h-screen place-items-center p-8 text-center", children: _jsx("p", { className: "text-sm opacity-70", ...(role ? { role } : {}), children: children }) }));
|
|
5
|
+
}
|
|
6
|
+
function signIn() {
|
|
7
|
+
return signInHref(`${window.location.pathname}${window.location.search}`);
|
|
8
|
+
}
|
|
9
|
+
export function SignIn() {
|
|
10
|
+
return (_jsx("main", { className: "grid min-h-screen place-items-center p-8 text-center", children: _jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsx("h1", { className: "text-xl font-semibold", children: "Sign in to Manta Horns" }), _jsx("a", { className: "rounded-md border px-4 py-2", href: signIn(), children: "Sign in" })] }) }));
|
|
11
|
+
}
|
|
12
|
+
/** A return from Keycloak carrying the marker but no session: the round trip did not complete. */
|
|
13
|
+
export function SignInLooped() {
|
|
14
|
+
return (_jsx("main", { className: "grid min-h-screen place-items-center p-8 text-center", children: _jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsx("p", { role: "alert", children: "Signing in did not complete. Try again, or ask an admin to check your access." }), _jsx("a", { className: "rounded-md border px-4 py-2", href: signIn(), children: "Sign in" })] }) }));
|
|
15
|
+
}
|