@shortlink-org/portolan 0.3.0 → 0.4.0
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/README.md +151 -6
- package/catalog/model.go +147 -0
- package/catalog/roundtrip_test.go +7 -0
- package/cli/portolan.mjs +6 -1
- package/go.mod +7 -0
- package/go.sum +20 -6
- package/package.json +10 -9
- package/plugin/describe.go +38 -0
- package/plugins/README.md +296 -5
- package/plugins/extract-argocd/options.schema.json +44 -0
- package/plugins/extract-celery/main.py +1 -0
- package/plugins/extract-csharp-ddd/README.md +213 -0
- package/plugins/extract-csharp-ddd/options.schema.json +55 -0
- package/plugins/extract-django/README.md +13 -2
- package/plugins/extract-django/choices.py +47 -0
- package/plugins/extract-django/domain.py +2 -1
- package/plugins/extract-django/extract_test.py +13 -0
- package/plugins/extract-django/lifecycle.py +2 -28
- package/plugins/extract-django/main.py +1 -0
- package/plugins/extract-django/rules.py +154 -0
- package/plugins/extract-django/rules_test.py +158 -0
- package/plugins/extract-go-sqs/options.schema.json +21 -0
- package/plugins/extract-http-clients/describe.go +5 -4
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-k8s/options.schema.json +32 -0
- package/plugins/extract-laravel/Cargo.lock +962 -0
- package/plugins/extract-laravel/Cargo.toml +20 -0
- package/plugins/extract-laravel/README.md +200 -0
- package/plugins/extract-laravel/options.schema.json +68 -0
- package/plugins/extract-laravel/rustfmt.toml +1 -0
- package/plugins/extract-php-ddd/Cargo.lock +972 -0
- package/plugins/extract-php-ddd/Cargo.toml +22 -0
- package/plugins/extract-php-ddd/README.md +141 -0
- package/plugins/extract-php-ddd/options.schema.json +50 -0
- package/plugins/extract-php-ddd/rustfmt.toml +1 -0
- package/plugins/extract-python-kafka/main.py +1 -0
- package/plugins/extract-terraform/options.schema.json +26 -0
- package/plugins/extract-ts/main.ts +1 -0
- package/plugins/phpscan/Cargo.lock +953 -0
- package/plugins/phpscan/Cargo.toml +19 -0
- package/plugins/phpscan/rustfmt.toml +1 -0
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/catalog.py +24 -1
- package/plugins/verify-otel/options.schema.json +12 -0
- package/portolan.json +130 -3
- package/rules/builtin.json +218 -0
- package/schema/portolan.schema.json +880 -13
- package/scripts/README.md +3 -1
- package/scripts/delivery-presets.mjs +36 -3
- package/scripts/gen-likec4.mjs +149 -16
- package/scripts/gen-likec4.test.mjs +96 -0
- package/scripts/gen.mjs +30 -3
- package/scripts/gitops-example.test.mjs +108 -0
- package/scripts/host-plugins/fetch-argocd.mjs +338 -0
- package/scripts/host-plugins/fetch-argocd.options.json +49 -0
- package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
- package/scripts/host-plugins/fetch-bsr.mjs +1 -0
- package/scripts/host-plugins/fetch-csr.mjs +1 -0
- package/scripts/host-plugins/fetch-git.mjs +1 -0
- package/scripts/host-plugins/fetch-k8s.mjs +263 -0
- package/scripts/host-plugins/fetch-k8s.options.json +50 -0
- package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
- package/scripts/host-plugins/k8s-topology.mjs +183 -0
- package/scripts/json-format.mjs +192 -0
- package/scripts/json-format.test.mjs +97 -0
- package/scripts/local-api.mjs +216 -8
- package/scripts/local-api.test.mjs +88 -2
- package/scripts/local-discovery.mjs +12 -0
- package/scripts/manifest.mjs +10 -1
- package/scripts/plugin-host.mjs +23 -1
- package/scripts/plugins-fresh.mjs +97 -0
- package/scripts/plugins-fresh.test.mjs +64 -0
- package/scripts/run-builtin.mjs +26 -5
- package/scripts/schema.mjs +138 -19
- package/scripts/trace-trials.mjs +176 -0
- package/scripts/trace-trials.test.mjs +142 -0
- package/scripts/warning-policy.mjs +12 -6
- package/scripts/warning-policy.test.mjs +40 -3
- package/src/app/Breadcrumbs.test.ts +3 -0
- package/src/app/Breadcrumbs.tsx +3 -0
- package/src/app/CatalogApp.tsx +2 -0
- package/src/app/SidebarFooter.tsx +20 -4
- package/src/catalog-index.ts +25 -0
- package/src/catalog-model.ts +246 -1
- package/src/catalog-validation.ts +104 -2
- package/src/catalog.test.ts +40 -1
- package/src/components/ChannelRows.test.tsx +76 -1
- package/src/components/ChannelRows.tsx +223 -60
- package/src/components/DeploymentRows.tsx +131 -0
- package/src/components/DocsLinks.test.tsx +27 -0
- package/src/components/DocsLinks.tsx +56 -0
- package/src/components/FieldTree.tsx +5 -2
- package/src/components/HTTPDestinationEvidence.tsx +1 -1
- package/src/components/PluginIcon.tsx +77 -0
- package/src/components/ProblemRow.tsx +173 -153
- package/src/components/RuleMarks.tsx +22 -0
- package/src/components/ShapeRows.tsx +24 -20
- package/src/data.ts +21 -0
- package/src/enrich.test.ts +127 -3
- package/src/enrich.ts +104 -6
- package/src/er/ErCanvas.tsx +218 -12
- package/src/er/GroupNode.tsx +57 -0
- package/src/er/StoreHeader.tsx +1 -0
- package/src/er/layout.test.ts +85 -2
- package/src/er/layout.ts +140 -5
- package/src/er/spec.test.ts +17 -0
- package/src/er/spec.ts +23 -10
- package/src/flow/Recordings.test.tsx +52 -0
- package/src/flow/Recordings.tsx +236 -0
- package/src/flow/StepDetail.tsx +53 -0
- package/src/flow/TraceTrial.tsx +419 -0
- package/src/flow/examples.test.ts +33 -0
- package/src/flow/examples.ts +37 -0
- package/src/flow/outline.test.ts +28 -0
- package/src/flow/outline.ts +7 -2
- package/src/flow/trace-trial-resume.test.ts +42 -0
- package/src/flow/trace-trial-resume.ts +74 -0
- package/src/graph/elk.ts +78 -0
- package/src/index.css +24 -0
- package/src/landing/LandingPage.tsx +15 -11
- package/src/landing/ProductTour.tsx +6 -6
- package/src/lib/all-problems.ts +27 -17
- package/src/lib/centrality.test.ts +251 -0
- package/src/lib/centrality.ts +232 -0
- package/src/lib/confluence.test.ts +37 -0
- package/src/lib/confluence.ts +41 -0
- package/src/lib/context-color.ts +1 -1
- package/src/lib/deployment-drift.ts +22 -0
- package/src/lib/derive.ts +43 -69
- package/src/lib/environments.test.ts +83 -0
- package/src/lib/environments.ts +57 -0
- package/src/lib/integration-url.test.ts +30 -0
- package/src/lib/integration-url.ts +63 -0
- package/src/lib/kafka-ui.ts +3 -45
- package/src/lib/local-api.ts +96 -2
- package/src/lib/notion.ts +13 -0
- package/src/lib/plugin-index.json +2765 -0
- package/src/lib/plugins.test.ts +68 -0
- package/src/lib/plugins.ts +264 -0
- package/src/lib/problem-flows.test.ts +61 -0
- package/src/lib/problem-flows.ts +78 -0
- package/src/lib/problem-rules-cel.d.mts +43 -0
- package/src/lib/problem-rules-cel.mjs +407 -0
- package/src/lib/problem-rules.test.ts +287 -0
- package/src/lib/problem-rules.ts +271 -0
- package/src/lib/problem-subjects.ts +737 -0
- package/src/lib/rule-entries.ts +39 -0
- package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
- package/src/lib/rules-deploy.test.ts +133 -0
- package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
- package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
- package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
- package/src/lib/rules.test.ts +51 -0
- package/src/lib/rules.ts +86 -0
- package/src/lib/shape.test.ts +32 -0
- package/src/lib/shape.ts +30 -6
- package/src/lib/tech.ts +16 -0
- package/src/lib/trace-project.test.ts +34 -0
- package/src/lib/trace-project.ts +50 -0
- package/src/lib/use-problems.ts +23 -0
- package/src/lib/warnings.test.ts +9 -0
- package/src/lib/warnings.ts +10 -10
- package/src/likec4/ids.test.ts +6 -2
- package/src/likec4/ids.ts +43 -0
- package/src/main.tsx +23 -0
- package/src/merge-deployments.test.ts +127 -0
- package/src/merge.test.ts +82 -0
- package/src/merge.ts +173 -9
- package/src/pages/AggregatePage.tsx +57 -7
- package/src/pages/ContextMap.tsx +45 -3
- package/src/pages/ContextPage.tsx +2 -0
- package/src/pages/EventPage.tsx +15 -5
- package/src/pages/FlowDetail.tsx +23 -2
- package/src/pages/GraphPage.tsx +40 -3
- package/src/pages/Overview.tsx +152 -12
- package/src/pages/PluginIndex.tsx +190 -0
- package/src/pages/Problems.tsx +396 -128
- package/src/pages/ServicePage.tsx +58 -2
- package/src/pages/Settings.tsx +28 -2
- package/src/pages/settings/AboutSettings.tsx +8 -1
- package/src/pages/settings/IntegrationsSettings.tsx +63 -17
- package/src/pages/settings/RecordingSettings.tsx +138 -0
- package/src/pages/settings/RulesSettings.tsx +825 -0
- package/src/routes.test.ts +9 -0
- package/src/routes.ts +22 -1
- package/src/lib/data-problems.ts +0 -314
- package/src/lib/proto-problems.ts +0 -237
- package/src/lib/wire-problems.ts +0 -342
package/src/pages/Overview.tsx
CHANGED
|
@@ -2,18 +2,25 @@ import { useDocumentTitle } from "../app/title";
|
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { Link } from "react-router";
|
|
4
4
|
import { AlertTriangle } from "lucide-react";
|
|
5
|
-
import { activeCatalogProfile, CATALOG_PATH, catalog
|
|
5
|
+
import { activeCatalogProfile, CATALOG_PATH, catalog } from "../data";
|
|
6
6
|
import { contextOwners, contextStats, widestFlows } from "../lib/derive";
|
|
7
7
|
import type { ContextOwner } from "../lib/derive";
|
|
8
|
-
import {
|
|
8
|
+
import { bridges } from "../lib/centrality";
|
|
9
|
+
import { useProblems } from "../lib/use-problems";
|
|
9
10
|
import { ctxStyle } from "../lib/context-color";
|
|
10
11
|
import { middleTruncate, plural } from "../lib/format";
|
|
11
12
|
import { useCountUp, staggerStyle } from "../lib/motion";
|
|
12
13
|
import { usePhone } from "../app/responsive";
|
|
13
|
-
import { CONTEXT_ANCHOR, OVERVIEW_ANCHOR, paths } from "../routes";
|
|
14
|
+
import { CONTEXT_ANCHOR, OVERVIEW_ANCHOR, paths, servicePath } from "../routes";
|
|
14
15
|
import { Blank, SectionTitle } from "../components/PageHeader";
|
|
15
16
|
import { C4View } from "../likec4/C4View";
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
deploymentViewId,
|
|
19
|
+
environmentsOf,
|
|
20
|
+
profileContainersViewId,
|
|
21
|
+
profileLandscapeViewId,
|
|
22
|
+
} from "../likec4/ids";
|
|
23
|
+
import { servicesDeployedIn } from "../lib/environments";
|
|
17
24
|
import { LevelSwitch } from "../likec4/levels";
|
|
18
25
|
import type { C4Level } from "../likec4/levels";
|
|
19
26
|
import { CatalogStamp } from "../components/CatalogStamp";
|
|
@@ -28,6 +35,9 @@ import {
|
|
|
28
35
|
/** How many problems the overview lists before pointing at the page that lists them all. */
|
|
29
36
|
const OVERVIEW_PROBLEMS = 4;
|
|
30
37
|
|
|
38
|
+
/** How many bridges the overview lists. Past the first few the shares are small and the list is the graph. */
|
|
39
|
+
const OVERVIEW_BRIDGES = 5;
|
|
40
|
+
|
|
31
41
|
/**
|
|
32
42
|
* A measurement that arrives rather than appears - 200ms, linear, once - and
|
|
33
43
|
* then takes you to what it measured. A number that counts something the app
|
|
@@ -96,12 +106,23 @@ export function Overview() {
|
|
|
96
106
|
useDocumentTitle("Overview");
|
|
97
107
|
const phone = usePhone();
|
|
98
108
|
const [level, setLevel] = useState<C4Level>(1);
|
|
109
|
+
// Where the estate runs, one environment at a time. Production first when
|
|
110
|
+
// there is one by that name: it is the environment a reader means by
|
|
111
|
+
// "deployed" until they say otherwise.
|
|
112
|
+
const environments = environmentsOf(catalog);
|
|
113
|
+
const [environment, setEnvironment] = useState<string>(
|
|
114
|
+
() =>
|
|
115
|
+
environments.find((e) => /^prod/i.test(e)) ?? environments[0] ?? "",
|
|
116
|
+
);
|
|
117
|
+
const deployedCount = (env: string) =>
|
|
118
|
+
servicesDeployedIn(catalog, new Set([env])).size;
|
|
99
119
|
const reach = widestFlows(catalog);
|
|
120
|
+
const roads = bridges(catalog);
|
|
100
121
|
const services = catalog.contexts.reduce(
|
|
101
122
|
(count, context) => count + context.services.length,
|
|
102
123
|
0,
|
|
103
124
|
);
|
|
104
|
-
const issues =
|
|
125
|
+
const issues = useProblems();
|
|
105
126
|
const issueCount = issues.length;
|
|
106
127
|
const errorCount = issues.filter((p) => p.severity === "error").length;
|
|
107
128
|
|
|
@@ -189,6 +210,44 @@ export function Overview() {
|
|
|
189
210
|
/>
|
|
190
211
|
</section>
|
|
191
212
|
|
|
213
|
+
{/* Where the estate runs, from the deployer's snapshot: the services
|
|
214
|
+
standing in one environment, in their clusters and namespaces, and
|
|
215
|
+
between them what the model declares. Absent entirely when no
|
|
216
|
+
snapshot places anything - a heading over an empty canvas would
|
|
217
|
+
claim the estate runs nowhere, and nobody made that claim. */}
|
|
218
|
+
{environments.length > 0 ? (
|
|
219
|
+
<section id={OVERVIEW_ANCHOR.deployed} className="mt-section">
|
|
220
|
+
<SectionTitle
|
|
221
|
+
anchor={OVERVIEW_ANCHOR.deployed}
|
|
222
|
+
right={
|
|
223
|
+
<div className="seg" role="group" aria-label="Environment">
|
|
224
|
+
{environments.map((env) => (
|
|
225
|
+
<button
|
|
226
|
+
key={env}
|
|
227
|
+
type="button"
|
|
228
|
+
onClick={() => setEnvironment(env)}
|
|
229
|
+
aria-pressed={env === environment}
|
|
230
|
+
className="flex items-center gap-1.5"
|
|
231
|
+
title={`${plural(deployedCount(env), "service")} of the estate in ${env}`}
|
|
232
|
+
>
|
|
233
|
+
{env}
|
|
234
|
+
<span className="tnum">{deployedCount(env)}</span>
|
|
235
|
+
</button>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
}
|
|
239
|
+
>
|
|
240
|
+
Deployed
|
|
241
|
+
</SectionTitle>
|
|
242
|
+
<C4View
|
|
243
|
+
viewId={deploymentViewId(environment)}
|
|
244
|
+
height={phone ? 320 : 480}
|
|
245
|
+
controls={phone}
|
|
246
|
+
fitViewPadding={{ x: 8, y: 8 }}
|
|
247
|
+
/>
|
|
248
|
+
</section>
|
|
249
|
+
) : null}
|
|
250
|
+
|
|
192
251
|
<section id={OVERVIEW_ANCHOR.contexts} className="mt-section">
|
|
193
252
|
<SectionTitle anchor={OVERVIEW_ANCHOR.contexts}>Contexts</SectionTitle>
|
|
194
253
|
<div
|
|
@@ -281,6 +340,85 @@ export function Overview() {
|
|
|
281
340
|
</div>
|
|
282
341
|
</section>
|
|
283
342
|
|
|
343
|
+
{/* The services the estate's paths run through: the share of shortest
|
|
344
|
+
paths between every other pair of services that pass through this
|
|
345
|
+
one, and the pairs of contexts it is the main road between. Degree
|
|
346
|
+
is not shown - a hub is visible on the graph, a bridge is not: the
|
|
347
|
+
service with three neighbours that is the only way from auth into
|
|
348
|
+
payments looks like nothing there. Nothing is shown for an estate
|
|
349
|
+
with no such service; a heading over "none" is a dead end. */}
|
|
350
|
+
{roads.length > 0 ? (
|
|
351
|
+
<section id={OVERVIEW_ANCHOR.bridges} className="mt-section">
|
|
352
|
+
<SectionTitle
|
|
353
|
+
anchor={OVERVIEW_ANCHOR.bridges}
|
|
354
|
+
right={
|
|
355
|
+
<span className="flex items-center gap-2">
|
|
356
|
+
{roads.length > OVERVIEW_BRIDGES ? (
|
|
357
|
+
<span className="section-aside">
|
|
358
|
+
{OVERVIEW_BRIDGES} of {roads.length}
|
|
359
|
+
</span>
|
|
360
|
+
) : null}
|
|
361
|
+
<Link
|
|
362
|
+
to={paths.graph()}
|
|
363
|
+
className="rounded-control px-1 text-accent hover:underline"
|
|
364
|
+
>
|
|
365
|
+
the graph →
|
|
366
|
+
</Link>
|
|
367
|
+
</span>
|
|
368
|
+
}
|
|
369
|
+
>
|
|
370
|
+
Bridges
|
|
371
|
+
</SectionTitle>
|
|
372
|
+
<div className="flex flex-col gap-2" data-nav-list>
|
|
373
|
+
{roads.slice(0, OVERVIEW_BRIDGES).map((road, i) => {
|
|
374
|
+
const to = servicePath(road.id);
|
|
375
|
+
const percent = Math.round(road.betweenness * 100);
|
|
376
|
+
return (
|
|
377
|
+
<Link
|
|
378
|
+
key={road.id}
|
|
379
|
+
to={to ?? paths.graph()}
|
|
380
|
+
data-nav-item
|
|
381
|
+
className="row stagger-in flex-wrap"
|
|
382
|
+
style={staggerStyle(i)}
|
|
383
|
+
title={`${percent}% of the shortest paths between other services run through ${road.id}`}
|
|
384
|
+
>
|
|
385
|
+
{/* The share, as a bar the width of the number, so the
|
|
386
|
+
list reads as a ranking before any number is read. */}
|
|
387
|
+
<span className="flex w-16 shrink-0 items-center gap-1.5">
|
|
388
|
+
<span className="h-1.5 flex-1 overflow-hidden rounded-full bg-surface">
|
|
389
|
+
<span
|
|
390
|
+
className="block h-full bg-accent"
|
|
391
|
+
style={{ width: `${Math.max(percent, 2)}%` }}
|
|
392
|
+
/>
|
|
393
|
+
</span>
|
|
394
|
+
<span className="mono tnum w-8 text-right text-muted">
|
|
395
|
+
{percent}%
|
|
396
|
+
</span>
|
|
397
|
+
</span>
|
|
398
|
+
<span className="font-semibold" title={road.id}>
|
|
399
|
+
{road.id}
|
|
400
|
+
</span>
|
|
401
|
+
<span className="mono text-muted">between</span>
|
|
402
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
403
|
+
{road.between.map((pair) => (
|
|
404
|
+
<span
|
|
405
|
+
key={`${pair.a}~${pair.b}`}
|
|
406
|
+
className="flex items-center gap-1"
|
|
407
|
+
title={`${Math.round(pair.share * 100)}% of the shortest paths from ${pair.a} to ${pair.b} run through ${road.id}`}
|
|
408
|
+
>
|
|
409
|
+
<ContextPill id={pair.a} />
|
|
410
|
+
<span className="text-muted">↔</span>
|
|
411
|
+
<ContextPill id={pair.b} />
|
|
412
|
+
</span>
|
|
413
|
+
))}
|
|
414
|
+
</div>
|
|
415
|
+
</Link>
|
|
416
|
+
);
|
|
417
|
+
})}
|
|
418
|
+
</div>
|
|
419
|
+
</section>
|
|
420
|
+
) : null}
|
|
421
|
+
|
|
284
422
|
<section id={OVERVIEW_ANCHOR.flows} className="mt-section">
|
|
285
423
|
<SectionTitle
|
|
286
424
|
anchor={OVERVIEW_ANCHOR.flows}
|
|
@@ -381,13 +519,15 @@ export function Overview() {
|
|
|
381
519
|
Problems
|
|
382
520
|
</SectionTitle>
|
|
383
521
|
<div className="flex max-w-table flex-col gap-2" data-nav-list>
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
522
|
+
<div className="divide-y divide-line overflow-hidden rounded-card border border-line shadow-xs">
|
|
523
|
+
{issues.slice(0, OVERVIEW_PROBLEMS).map((problem, i) => (
|
|
524
|
+
<ProblemRow
|
|
525
|
+
key={`${problem.rule}:${problem.id}:${problem.peer}`}
|
|
526
|
+
problem={problem}
|
|
527
|
+
index={i}
|
|
528
|
+
/>
|
|
529
|
+
))}
|
|
530
|
+
</div>
|
|
391
531
|
{issueCount > OVERVIEW_PROBLEMS ? (
|
|
392
532
|
<Link
|
|
393
533
|
to={paths.problems()}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { useDocumentTitle } from "../app/title";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Link, useLocation } from "react-router";
|
|
4
|
+
import { ArrowUpRight, ShieldCheck, Terminal } from "lucide-react";
|
|
5
|
+
import { CatIllustration } from "../components/CatIllustration";
|
|
6
|
+
import { SectionTitle } from "../components/PageHeader";
|
|
7
|
+
import { PluginIcon } from "../components/PluginIcon";
|
|
8
|
+
import {
|
|
9
|
+
pluginsByCategory,
|
|
10
|
+
pluginIcon,
|
|
11
|
+
pluginLabel,
|
|
12
|
+
pluginSourceHref,
|
|
13
|
+
runtimeLabel,
|
|
14
|
+
} from "../lib/plugins";
|
|
15
|
+
import type { PluginEntry } from "../lib/plugins";
|
|
16
|
+
import { PRODUCT_REPOSITORY } from "../lib/product";
|
|
17
|
+
import { paths } from "../routes";
|
|
18
|
+
|
|
19
|
+
const PHASE_LABEL = { extract: "extract", verify: "verify", generate: "generate" } as const;
|
|
20
|
+
|
|
21
|
+
/** Every cat the site has drawn; any of them suits a page that is a list. */
|
|
22
|
+
const CAT_SCENES = ["about", "clear", "unchanged", "onboarding", "search", "trial"] as const;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Every plugin the package ships, grouped by what it reads or what it makes.
|
|
26
|
+
*
|
|
27
|
+
* Rendered from the index `npm run schema` writes, so this page says what the
|
|
28
|
+
* plugins say about themselves and nothing else: the summary each answers to
|
|
29
|
+
* `describe`, the category it sorts itself under, the options it can be told.
|
|
30
|
+
* The Settings page shows which of these a build ran; this one shows what a
|
|
31
|
+
* build could run.
|
|
32
|
+
*/
|
|
33
|
+
export function PluginIndex() {
|
|
34
|
+
useDocumentTitle("Plugins");
|
|
35
|
+
const groups = pluginsByCategory();
|
|
36
|
+
const total = groups.reduce((n, group) => n + group.plugins.length, 0);
|
|
37
|
+
const { hash } = useLocation();
|
|
38
|
+
const target = hash.startsWith("#plugin-") ? hash.slice("#plugin-".length) : null;
|
|
39
|
+
// A different cat each visit, the same one for the visit: picked once, not
|
|
40
|
+
// on every render, or a hash change would swap it mid-scroll.
|
|
41
|
+
const [cat] = useState(() => CAT_SCENES[Math.floor(Math.random() * CAT_SCENES.length)] ?? "about");
|
|
42
|
+
|
|
43
|
+
// A link from Settings names one plugin. The card grid settles a frame after
|
|
44
|
+
// the route commits, so the scroll waits for it rather than landing where
|
|
45
|
+
// the card was in a layout that no longer exists.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!hash) return;
|
|
48
|
+
const id = hash.slice(1);
|
|
49
|
+
const frame = requestAnimationFrame(() => {
|
|
50
|
+
document.getElementById(id)?.scrollIntoView({ block: "start" });
|
|
51
|
+
});
|
|
52
|
+
return () => cancelAnimationFrame(frame);
|
|
53
|
+
}, [hash]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="h-full overflow-y-auto p-gutter">
|
|
57
|
+
{/* The intro is a paragraph wide; on a wide screen the rest of the row
|
|
58
|
+
was empty, and a cat is a better use of it than nothing. */}
|
|
59
|
+
<div className="grid gap-grid xl:grid-cols-[minmax(0,1fr)_auto] xl:items-center">
|
|
60
|
+
<div>
|
|
61
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
62
|
+
<h1 className="text-lg font-semibold">Plugins</h1>
|
|
63
|
+
<span className="mono text-muted">{total} shipped</span>
|
|
64
|
+
</div>
|
|
65
|
+
<p className="mt-2 max-w-prose text-muted">
|
|
66
|
+
Every fact in the catalog was read by one of these. An extractor reads a
|
|
67
|
+
tree and answers with a fragment; a verifier checks the catalog against
|
|
68
|
+
evidence; a generator turns it into something else. Each describes
|
|
69
|
+
itself, and this page is that description. Which of them a build ran is
|
|
70
|
+
on the{" "}
|
|
71
|
+
<Link to={paths.settingsPipeline()} className="text-accent hover:underline">
|
|
72
|
+
pipeline settings
|
|
73
|
+
</Link>
|
|
74
|
+
; how to write one is in the{" "}
|
|
75
|
+
<a
|
|
76
|
+
href={`${PRODUCT_REPOSITORY}/tree/main/plugins#readme`}
|
|
77
|
+
target="_blank"
|
|
78
|
+
rel="noreferrer"
|
|
79
|
+
className="text-accent hover:underline"
|
|
80
|
+
>
|
|
81
|
+
plugin protocol
|
|
82
|
+
</a>
|
|
83
|
+
.
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
<CatIllustration scene={cat} className="hidden xl:block h-[126px] w-[180px] justify-self-end" />
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<nav aria-label="Categories" className="mt-4 flex flex-wrap gap-1.5">
|
|
90
|
+
{groups.map((group) => (
|
|
91
|
+
<a key={group.category} href={`#${group.category}`} className="chip inline-flex items-center gap-1.5 border-line-strong hover:border-accent hover:text-accent">
|
|
92
|
+
<PluginIcon icon={{ lucide: group.icon }} size={12} />
|
|
93
|
+
{group.title} <span className="text-muted/70">{group.plugins.length}</span>
|
|
94
|
+
</a>
|
|
95
|
+
))}
|
|
96
|
+
</nav>
|
|
97
|
+
|
|
98
|
+
<div className="mt-section space-y-section">
|
|
99
|
+
{groups.map((group) => (
|
|
100
|
+
<section key={group.category} id={group.category} className="scroll-mt-4">
|
|
101
|
+
<SectionTitle anchor={group.category} right={<span className="mono text-muted">{group.plugins.length}</span>}>
|
|
102
|
+
<span className="inline-flex items-center gap-2">
|
|
103
|
+
<PluginIcon icon={{ lucide: group.icon }} className="text-muted" />
|
|
104
|
+
{group.title}
|
|
105
|
+
</span>
|
|
106
|
+
</SectionTitle>
|
|
107
|
+
<p className="mb-3 max-w-prose text-muted">{group.what}</p>
|
|
108
|
+
<div className="grid gap-grid grid-cols-[repeat(auto-fill,minmax(340px,1fr))]">
|
|
109
|
+
{group.plugins.map((entry) => (
|
|
110
|
+
<PluginCard key={entry.name} entry={entry} highlighted={entry.name === target} />
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
</section>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function PluginCard({ entry, highlighted }: { entry: PluginEntry; highlighted: boolean }) {
|
|
121
|
+
const options = Object.entries(entry.options.properties ?? {});
|
|
122
|
+
const required = new Set(entry.options.required ?? []);
|
|
123
|
+
const RuntimeIcon = entry.runtime === "wasm" ? ShieldCheck : Terminal;
|
|
124
|
+
const runtimeTone = entry.runtime === "wasm" ? "text-verified" : "text-declared";
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<article
|
|
128
|
+
id={`plugin-${entry.name}`}
|
|
129
|
+
// The margin clears the section head, which stays stuck to the top
|
|
130
|
+
// while its cards scroll under it.
|
|
131
|
+
className={`card card-static flex scroll-mt-14 flex-col gap-3 ${highlighted ? "border-accent" : ""}`}
|
|
132
|
+
>
|
|
133
|
+
<header className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
134
|
+
<PluginIcon icon={pluginIcon(entry.name, entry.category)} size={16} className="text-ink" />
|
|
135
|
+
<h3 className="font-semibold text-ink">{pluginLabel(entry.name)}</h3>
|
|
136
|
+
<span className="mono text-muted" title="the name a manifest step uses">{entry.name}</span>
|
|
137
|
+
{entry.plugin !== entry.name ? (
|
|
138
|
+
<span className="mono text-faint" title="the plugin's own name">{entry.plugin}</span>
|
|
139
|
+
) : null}
|
|
140
|
+
</header>
|
|
141
|
+
|
|
142
|
+
<p className="text-muted">{entry.summary}</p>
|
|
143
|
+
|
|
144
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
145
|
+
{entry.phases.map((phase) => (
|
|
146
|
+
<span key={phase} className="chip border-line-strong">{PHASE_LABEL[phase]}</span>
|
|
147
|
+
))}
|
|
148
|
+
<span className={`chip inline-flex items-center gap-1 border-line-strong ${runtimeTone}`}>
|
|
149
|
+
<RuntimeIcon size={12} aria-hidden /> {runtimeLabel(entry)}
|
|
150
|
+
</span>
|
|
151
|
+
{entry.needs?.map((need) => (
|
|
152
|
+
<span key={need} className="chip border-line-strong" title="asked of the host beyond the tree">
|
|
153
|
+
needs {need}
|
|
154
|
+
</span>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{options.length > 0 ? (
|
|
159
|
+
<details className="group">
|
|
160
|
+
<summary className="mono cursor-pointer list-none text-muted hover:text-ink">
|
|
161
|
+
{options.length} option{options.length === 1 ? "" : "s"}
|
|
162
|
+
<span className="ml-1 inline-block transition-transform group-open:rotate-90">›</span>
|
|
163
|
+
</summary>
|
|
164
|
+
<dl className="mt-2 space-y-2 border-l border-line pl-3">
|
|
165
|
+
{options.map(([key, option]) => (
|
|
166
|
+
<div key={key}>
|
|
167
|
+
<dt className="mono text-ink">
|
|
168
|
+
{key}
|
|
169
|
+
{required.has(key) ? <span className="ml-1 text-faint" title="required">*</span> : null}
|
|
170
|
+
</dt>
|
|
171
|
+
{option.description ? <dd className="text-muted">{option.description}</dd> : null}
|
|
172
|
+
</div>
|
|
173
|
+
))}
|
|
174
|
+
</dl>
|
|
175
|
+
</details>
|
|
176
|
+
) : (
|
|
177
|
+
<p className="mono text-faint">takes no options</p>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
<a
|
|
181
|
+
href={pluginSourceHref(entry)}
|
|
182
|
+
target="_blank"
|
|
183
|
+
rel="noreferrer"
|
|
184
|
+
className="mono mt-auto inline-flex items-center gap-1 text-accent hover:underline"
|
|
185
|
+
>
|
|
186
|
+
{entry.source} <ArrowUpRight size={12} aria-hidden />
|
|
187
|
+
</a>
|
|
188
|
+
</article>
|
|
189
|
+
);
|
|
190
|
+
}
|