@shortlink-org/portolan 0.2.4 → 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 +174 -6
- package/catalog/enum_test.go +46 -0
- package/catalog/evidence_test.go +35 -0
- package/catalog/model.go +1213 -0
- package/catalog/roundtrip_test.go +210 -0
- package/catalog/via_test.go +38 -0
- package/cli/init.test.mjs +6 -1
- package/cli/portolan.mjs +14 -1
- package/cli/portolan.test.mjs +49 -0
- package/go.mod +21 -0
- package/go.sum +34 -0
- package/internal/gocall/README.md +19 -0
- package/internal/gocall/analyze.go +189 -0
- package/internal/gocall/analyze_test.go +107 -0
- package/internal/gohttp/analyze.go +2562 -0
- package/internal/gohttp/destination.go +373 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +96 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/index.go +629 -0
- package/internal/goscan/index_test.go +66 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +37 -0
- package/internal/goscan/tree.go +284 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +70 -0
- package/internal/wsdl/parse.go +949 -0
- package/internal/wsdl/parse_test.go +170 -0
- package/package.json +28 -15
- package/plugin/describe.go +156 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +141 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +404 -50
- package/plugins/cmd/portolan-http-clients/main.go +19 -0
- package/plugins/extract-argocd/options.schema.json +44 -0
- package/plugins/extract-celery/extract.py +0 -2
- package/plugins/extract-celery/extract_test.py +1 -1
- 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 +52 -19
- package/plugins/extract-django/choices.py +47 -0
- package/plugins/extract-django/domain.py +30 -18
- package/plugins/extract-django/extract.py +21 -7
- package/plugins/extract-django/extract_test.py +68 -2
- package/plugins/extract-django/lifecycle.py +4 -28
- package/plugins/extract-django/main.py +1 -0
- package/plugins/extract-django/operations.py +1 -1
- package/plugins/extract-django/routing_test.py +109 -1
- package/plugins/extract-django/rules.py +154 -0
- package/plugins/extract-django/rules_test.py +158 -0
- package/plugins/extract-django/store.py +1 -1
- package/plugins/extract-django/transport.py +101 -55
- package/plugins/extract-django/verbs.py +241 -0
- package/plugins/extract-go/README.md +47 -0
- package/plugins/extract-go-sqs/options.schema.json +21 -0
- package/plugins/extract-http-clients/describe.go +20 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +740 -0
- package/plugins/extract-http-clients/extract_test.go +1561 -0
- package/plugins/extract-http-clients/main.go +41 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.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/extract.py +0 -2
- package/plugins/extract-python-kafka/extract_test.py +1 -1
- package/plugins/extract-python-kafka/main.py +1 -0
- package/plugins/extract-terraform/options.schema.json +26 -0
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/extract.ts +4 -5
- package/plugins/extract-ts/graphql.test.ts +1 -1
- package/plugins/extract-ts/main.ts +1 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -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/pyplugin/protocol.py +1 -5
- package/plugins/verify-otel/options.schema.json +12 -0
- package/portolan.json +132 -4
- package/rules/builtin.json +218 -0
- package/schema/portolan.schema.json +905 -4
- package/scripts/README.md +21 -13
- package/scripts/catalog-sources.mjs +6 -0
- package/scripts/delivery-presets.mjs +57 -14
- package/scripts/diff.mjs +5 -1
- package/scripts/django-aggregates.test.mjs +58 -0
- package/scripts/gen-likec4.mjs +150 -17
- package/scripts/gen-likec4.test.mjs +96 -0
- package/scripts/gen.mjs +148 -118
- package/scripts/gitops-example.test.mjs +108 -0
- package/scripts/go-discovery.test.mjs +30 -0
- package/scripts/history.mjs +186 -3
- package/scripts/history.test.mjs +1 -1
- 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 +78 -21
- package/scripts/host-plugins/fetch-git.test.mjs +62 -8
- 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 +287 -12
- package/scripts/local-api.test.mjs +150 -5
- package/scripts/local-discovery.mjs +94 -9
- package/scripts/manifest.mjs +14 -3
- package/scripts/manifest.test.mjs +24 -0
- package/scripts/output-diff.mjs +94 -0
- package/scripts/output-diff.test.mjs +36 -0
- package/scripts/package-smoke.mjs +62 -4
- package/scripts/plugin-host.mjs +45 -3
- package/scripts/plugin-host.test.mjs +9 -0
- package/scripts/plugin-wasm-worker.mjs +4 -1
- package/scripts/plugins-fresh.mjs +97 -0
- package/scripts/plugins-fresh.test.mjs +64 -0
- package/scripts/provenance.mjs +72 -0
- package/scripts/provenance.test.mjs +149 -0
- package/scripts/run-builtin.mjs +62 -7
- package/scripts/schema.mjs +167 -19
- package/scripts/trace-trials.mjs +176 -0
- package/scripts/trace-trials.test.mjs +142 -0
- package/scripts/warning-policy.mjs +167 -0
- package/scripts/warning-policy.test.mjs +93 -0
- 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/Sidebar.tsx +3 -3
- package/src/app/SidebarFooter.tsx +20 -4
- package/src/catalog-docs.test.ts +64 -0
- package/src/catalog-docs.ts +35 -0
- package/src/catalog-error.test.ts +15 -0
- package/src/catalog-index.ts +25 -0
- package/src/catalog-model.ts +293 -5
- package/src/catalog-validation.ts +113 -2
- package/src/catalog.test.ts +40 -1
- package/src/chat/Starter.tsx +5 -11
- package/src/chat/tools.test.ts +27 -0
- package/src/chat/tools.ts +5 -9
- package/src/components/CatalogStamp.tsx +10 -8
- 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.test.tsx +23 -0
- package/src/components/HTTPDestinationEvidence.tsx +31 -0
- package/src/components/Integrations.tsx +1 -1
- package/src/components/MachineDocs.tsx +6 -5
- package/src/components/MethodRows.tsx +9 -2
- package/src/components/PluginIcon.tsx +77 -0
- package/src/components/ProblemRow.tsx +173 -153
- package/src/components/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/components/RuleMarks.tsx +22 -0
- package/src/components/ShapeRows.tsx +24 -20
- package/src/data.ts +46 -7
- package/src/enrich.test.ts +459 -4
- package/src/enrich.ts +308 -7
- 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 +59 -0
- package/src/flow/TraceTrial.tsx +419 -0
- package/src/flow/evidence.test.ts +16 -0
- package/src/flow/evidence.ts +34 -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 +68 -0
- package/src/landing/DraggableReveal.tsx +3 -2
- package/src/landing/EvidencePipeline.tsx +105 -0
- package/src/landing/LandingPage.tsx +17 -70
- package/src/landing/ProductTour.tsx +6 -6
- package/src/lib/all-problems.ts +27 -17
- package/src/lib/catalog-diff.ts +1 -1
- 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/django-aggregates.d.mts +9 -0
- package/src/lib/django-aggregates.mjs +36 -0
- package/src/lib/django-aggregates.test.ts +29 -0
- package/src/lib/django-aggregates.ts +5 -0
- 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 +116 -4
- 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/setup-info.test.ts +17 -0
- package/src/lib/setup-info.ts +58 -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 +63 -0
- package/src/lib/warnings.ts +260 -0
- package/src/likec4/ids.test.ts +6 -2
- package/src/likec4/ids.ts +43 -0
- package/src/main.tsx +23 -0
- package/src/map/ContextMapGraph.tsx +76 -32
- package/src/merge-deployments.test.ts +127 -0
- package/src/merge.test.ts +82 -0
- package/src/merge.ts +189 -18
- package/src/pages/AggregatePage.tsx +65 -14
- package/src/pages/ContextMap.tsx +45 -3
- package/src/pages/ContextPage.tsx +8 -5
- 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 +62 -5
- package/src/pages/Settings.tsx +217 -43
- package/src/pages/settings/AboutSettings.tsx +8 -1
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- 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/selection/DetailPanel.tsx +15 -0
- package/src/virtual-provenance.d.ts +11 -0
- package/vite.config.ts +5 -0
- package/scripts/vendor-lock.mjs +0 -58
- package/scripts/vendor-lock.test.mjs +0 -69
- package/src/lib/data-problems.ts +0 -314
- package/src/lib/proto-problems.ts +0 -237
- package/src/lib/wire-problems.ts +0 -342
|
@@ -9,6 +9,7 @@ import { staggerStyle } from "../lib/motion";
|
|
|
9
9
|
import { CONTEXT_ANCHOR, EVENT_ANCHOR, LINKS_HERE, paths } from "../routes";
|
|
10
10
|
import { Empty, PageHeader, SectionTitle } from "../components/PageHeader";
|
|
11
11
|
import { Ident } from "../components/Ident";
|
|
12
|
+
import { DocsLinks } from "../components/DocsLinks";
|
|
12
13
|
import { ClassificationBadge } from "../components/primitives";
|
|
13
14
|
import { KindIcon } from "../components/kind";
|
|
14
15
|
import { RowActions } from "../components/RowActions";
|
|
@@ -37,6 +38,7 @@ export function ContextPage() {
|
|
|
37
38
|
);
|
|
38
39
|
const neutral = groupKind(context) !== "bounded-context";
|
|
39
40
|
const showDomain = !neutral || aggregates.length > 0;
|
|
41
|
+
const hasModelGroups = aggregates.some(({ aggregate }) => aggregate.kind === "model-group");
|
|
40
42
|
const toc: TocItem[] = [
|
|
41
43
|
{ id: CONTEXT_ANCHOR.services, label: neutral ? "Components" : "Services" },
|
|
42
44
|
...(showDomain
|
|
@@ -86,13 +88,14 @@ export function ContextPage() {
|
|
|
86
88
|
{showDomain ? (
|
|
87
89
|
<>
|
|
88
90
|
<a href={`#${CONTEXT_ANCHOR.aggregates}`} className="rounded-control hover:text-ink">
|
|
89
|
-
<span className="tnum">{stats.aggregates}</span>{" "}{plural(stats.aggregates, "aggregate")}
|
|
91
|
+
<span className="tnum">{stats.aggregates}</span>{" "}{plural(stats.aggregates, hasModelGroups ? "group" : "aggregate")}
|
|
90
92
|
</a>
|
|
91
93
|
<a href={`#${CONTEXT_ANCHOR.events}`} className="rounded-control hover:text-ink">
|
|
92
94
|
<span className="tnum">{stats.events}</span>{" "}{plural(stats.events, "event")}
|
|
93
95
|
</a>
|
|
94
96
|
</>
|
|
95
97
|
) : null}
|
|
98
|
+
<DocsLinks name={context.name} />
|
|
96
99
|
</div>
|
|
97
100
|
{/* Who to ask about the whole domain, folded up from what its
|
|
98
101
|
services' CODEOWNERS say - the context itself names nobody. The
|
|
@@ -173,7 +176,7 @@ export function ContextPage() {
|
|
|
173
176
|
<div className="mono mt-4 flex flex-wrap gap-x-4 text-muted">
|
|
174
177
|
{service.aggregates.length > 0 || componentKind(service) === "service" ? (
|
|
175
178
|
<Link to={`${paths.service(context.id, service.slug)}#svc-aggregates`} className="rounded-control hover:text-ink">
|
|
176
|
-
<span className="tnum">{service.aggregates.length}</span>{" "}{plural(service.aggregates.length, "aggregate")}
|
|
179
|
+
<span className="tnum">{service.aggregates.length}</span>{" "}{plural(service.aggregates.length, service.aggregates.some((aggregate) => aggregate.kind === "model-group") ? "group" : "aggregate")}
|
|
177
180
|
</Link>
|
|
178
181
|
) : null}
|
|
179
182
|
<Link
|
|
@@ -234,11 +237,11 @@ export function ContextPage() {
|
|
|
234
237
|
anchor={CONTEXT_ANCHOR.aggregates}
|
|
235
238
|
right={
|
|
236
239
|
<span>
|
|
237
|
-
every aggregate this domain owns, whichever service holds it
|
|
240
|
+
{hasModelGroups ? "models grouped by their source application or aggregate" : "every aggregate this domain owns, whichever service holds it"}
|
|
238
241
|
</span>
|
|
239
242
|
}
|
|
240
243
|
>
|
|
241
|
-
Aggregates
|
|
244
|
+
{hasModelGroups ? "Aggregates and model groups" : "Aggregates"}
|
|
242
245
|
</SectionTitle>
|
|
243
246
|
{aggregates.length === 0 ? (
|
|
244
247
|
<Empty>this domain owns nothing yet — only services</Empty>
|
|
@@ -264,7 +267,7 @@ export function ContextPage() {
|
|
|
264
267
|
{aggregate.name}
|
|
265
268
|
</Link>
|
|
266
269
|
<span className="meta">{service.slug}</span>
|
|
267
|
-
<span className="meta">root {aggregate.root}</span>
|
|
270
|
+
<span className="meta">{aggregate.kind === "model-group" ? "model group · boundary not specified" : `root ${aggregate.root}`}</span>
|
|
268
271
|
<RowActions
|
|
269
272
|
copy={aggregate.id}
|
|
270
273
|
reveal={aggregate.id}
|
package/src/pages/EventPage.tsx
CHANGED
|
@@ -48,6 +48,8 @@ interface SchemaRow extends Field {
|
|
|
48
48
|
change?: Change;
|
|
49
49
|
/** The type the previous version gave the field, when this one changed it. */
|
|
50
50
|
from?: string;
|
|
51
|
+
/** The rules the previous version gave the field, when this one changed them; "" for none. */
|
|
52
|
+
rulesFrom?: string;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/** What the change column says, and in what colour. */
|
|
@@ -60,7 +62,7 @@ const CHANGE: Record<Change, { label: string; className: string; title: string }
|
|
|
60
62
|
changed: {
|
|
61
63
|
label: "changed",
|
|
62
64
|
className: "text-declared",
|
|
63
|
-
title: "
|
|
65
|
+
title: "changed in this version",
|
|
64
66
|
},
|
|
65
67
|
removed: {
|
|
66
68
|
label: "removed",
|
|
@@ -69,13 +71,21 @@ const CHANGE: Record<Change, { label: string; className: string; title: string }
|
|
|
69
71
|
},
|
|
70
72
|
};
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* What the previous version had, for the tooltip of a changed row: the type
|
|
76
|
+
* when it moved, the rules when they did. What the row has now is the row.
|
|
77
|
+
*/
|
|
78
|
+
function wasBefore(row: SchemaRow): string | null {
|
|
79
|
+
const parts: string[] = [];
|
|
80
|
+
if (row.from) parts.push(`was ${row.from}`);
|
|
81
|
+
if (row.rulesFrom !== undefined) parts.push(`rules were ${row.rulesFrom || "none"}`);
|
|
82
|
+
return parts.length ? `${parts.join(", ")} in the previous version` : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
function ChangeMark({ row }: { row: SchemaRow }) {
|
|
73
86
|
if (!row.change) return null;
|
|
74
87
|
const mark = CHANGE[row.change];
|
|
75
|
-
const title =
|
|
76
|
-
row.change === "changed" && row.from
|
|
77
|
-
? `was ${row.from} in the previous version`
|
|
78
|
-
: mark.title;
|
|
88
|
+
const title = (row.change === "changed" && wasBefore(row)) || mark.title;
|
|
79
89
|
return (
|
|
80
90
|
<span
|
|
81
91
|
className={`mono inline-flex items-center gap-1 ${mark.className}`}
|
package/src/pages/FlowDetail.tsx
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
isCrossContext,
|
|
31
31
|
} from "../flow/cross-context";
|
|
32
32
|
import { StepRail } from "../flow/StepRail";
|
|
33
|
+
import { RecordingsChip } from "../flow/Recordings";
|
|
34
|
+
import { stepsShownBy } from "../flow/examples";
|
|
33
35
|
import { FlowTable } from "../flow/FlowTable";
|
|
34
36
|
import { FlowToolbar } from "../flow/FlowToolbar";
|
|
35
37
|
import { buildChapters, groupRows } from "../flow/chapters";
|
|
@@ -127,6 +129,8 @@ export function FlowDetail() {
|
|
|
127
129
|
const [hoverStep, setHoverStep] = useState<string | null>(null);
|
|
128
130
|
/** The step LikeC4's walkthrough is on, when it is running. */
|
|
129
131
|
const [walkStep, setWalkStep] = useState<string | null>(null);
|
|
132
|
+
/** The recording whose steps are lit on the picture, when one is chosen. */
|
|
133
|
+
const [exampleId, setExampleId] = useState<string | null>(null);
|
|
130
134
|
|
|
131
135
|
const canvas = useRef<CanvasHandle | null>(null);
|
|
132
136
|
|
|
@@ -264,6 +268,17 @@ export function FlowDetail() {
|
|
|
264
268
|
// watching rather than pointing, and the rail's job is to follow.
|
|
265
269
|
const activeId = walkStep ?? selectedStepId ?? focusedMatch;
|
|
266
270
|
|
|
271
|
+
/**
|
|
272
|
+
* The steps a chosen recording showed. A recording is a reading of the
|
|
273
|
+
* flow like a path is, and it lights the picture the same way, below
|
|
274
|
+
* whatever the reader is pointing at.
|
|
275
|
+
*/
|
|
276
|
+
const exampleSteps = useMemo(() => {
|
|
277
|
+
const example = exampleId
|
|
278
|
+
? flow?.examples?.find((e) => e.id === exampleId)
|
|
279
|
+
: undefined;
|
|
280
|
+
return example ? stepsShownBy(example) : [];
|
|
281
|
+
}, [flow, exampleId]);
|
|
267
282
|
const litSteps = useMemo(
|
|
268
283
|
() =>
|
|
269
284
|
walkStep
|
|
@@ -272,8 +287,10 @@ export function FlowDetail() {
|
|
|
272
287
|
? [selectedStepId]
|
|
273
288
|
: hoverStep
|
|
274
289
|
? [hoverStep]
|
|
275
|
-
: matches
|
|
276
|
-
|
|
290
|
+
: matches.length > 0
|
|
291
|
+
? matches
|
|
292
|
+
: exampleSteps,
|
|
293
|
+
[walkStep, selectedStepId, hoverStep, matches, exampleSteps],
|
|
277
294
|
);
|
|
278
295
|
|
|
279
296
|
useEffect(() => {
|
|
@@ -511,6 +528,10 @@ export function FlowDetail() {
|
|
|
511
528
|
</a>
|
|
512
529
|
) : null}
|
|
513
530
|
<EditorLink location={flowSource} variant="text" />
|
|
531
|
+
{/* A recording is evidence too: the traces the flow was seen
|
|
532
|
+
running in, opened behind a chip rather than laid out in the
|
|
533
|
+
rail, which is for the steps. */}
|
|
534
|
+
<RecordingsChip flow={flow} exampleId={exampleId} onExample={setExampleId} />
|
|
514
535
|
<WhatLinksHere
|
|
515
536
|
target={{ kind: "flow", id: flow.slug }}
|
|
516
537
|
variant="line"
|
package/src/pages/GraphPage.tsx
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
filterEventGraph,
|
|
12
12
|
} from "../lib/event-graph";
|
|
13
13
|
import { contextVar } from "../lib/context-color";
|
|
14
|
+
import { narrowToEnvironments } from "../lib/environments";
|
|
15
|
+
import { environmentsOf } from "../likec4/ids";
|
|
14
16
|
import { statusColor, statusDash } from "../graph/theme";
|
|
15
17
|
import { DependencyGraphPane } from "../graph/DependencyGraph";
|
|
16
18
|
import type { GraphMode } from "../graph/dependency-layout";
|
|
@@ -28,8 +30,16 @@ export function GraphPage() {
|
|
|
28
30
|
const [contexts, setContexts] = useState<Set<string>>(new Set());
|
|
29
31
|
const [statuses, setStatuses] = useState<Set<Status>>(new Set());
|
|
30
32
|
const [mode, setMode] = useState<GraphMode>("bipartite");
|
|
33
|
+
// Where the estate runs, when a snapshot says. Narrowing the catalog
|
|
34
|
+
// rather than the graph: a service not deployed in staging is not a node
|
|
35
|
+
// to fade but a service that is not there, and every count follows.
|
|
36
|
+
const environmentChips = useMemo(() => environmentsOf(catalog), []);
|
|
37
|
+
const [environments, setEnvironments] = useState<Set<string>>(new Set());
|
|
31
38
|
|
|
32
|
-
const whole = useMemo(
|
|
39
|
+
const whole = useMemo(
|
|
40
|
+
() => eventGraph(narrowToEnvironments(catalog, environments)),
|
|
41
|
+
[environments],
|
|
42
|
+
);
|
|
33
43
|
const graph = useMemo(
|
|
34
44
|
() => filterEventGraph(whole, { contexts, statuses }),
|
|
35
45
|
[whole, contexts, statuses],
|
|
@@ -61,7 +71,8 @@ export function GraphPage() {
|
|
|
61
71
|
// pills with no line between them reads as a rendering failure rather than
|
|
62
72
|
// as an answer. Either the filters left no event - the common case, and the
|
|
63
73
|
// reader wants the way back - or the catalog has none yet.
|
|
64
|
-
const filtered =
|
|
74
|
+
const filtered =
|
|
75
|
+
contexts.size > 0 || statuses.size > 0 || environments.size > 0;
|
|
65
76
|
const nothing = graph.events.length === 0;
|
|
66
77
|
|
|
67
78
|
const toggle = <T,>(set: Set<T>, value: T): Set<T> => {
|
|
@@ -74,7 +85,7 @@ export function GraphPage() {
|
|
|
74
85
|
// Filters and mode both replace the layout wholesale, so both refit. Nothing
|
|
75
86
|
// else does: a selection or a focus leaves the viewport where the reader put
|
|
76
87
|
// it.
|
|
77
|
-
const fitKey = `${[...contexts].sort().join(",")}|${[...statuses].sort().join(",")}|${mode}`;
|
|
88
|
+
const fitKey = `${[...contexts].sort().join(",")}|${[...statuses].sort().join(",")}|${[...environments].sort().join(",")}|${mode}`;
|
|
78
89
|
|
|
79
90
|
return (
|
|
80
91
|
<div className="flex h-full flex-col">
|
|
@@ -123,6 +134,32 @@ export function GraphPage() {
|
|
|
123
134
|
})}
|
|
124
135
|
</div>
|
|
125
136
|
|
|
137
|
+
{environmentChips.length > 0 ? (
|
|
138
|
+
<div className="seg" role="group" aria-label="Filter by environment">
|
|
139
|
+
{environmentChips.map((env) => {
|
|
140
|
+
const on = environments.has(env);
|
|
141
|
+
return (
|
|
142
|
+
<button
|
|
143
|
+
key={env}
|
|
144
|
+
type="button"
|
|
145
|
+
onClick={() => setEnvironments((prev) => toggle(prev, env))}
|
|
146
|
+
aria-pressed={on}
|
|
147
|
+
title={`only the services the deployer places in ${env}`}
|
|
148
|
+
className="flex items-center gap-1.5"
|
|
149
|
+
style={{
|
|
150
|
+
color: on ? "var(--accent)" : "var(--fg-muted)",
|
|
151
|
+
background: on
|
|
152
|
+
? "color-mix(in srgb, var(--accent) 12%, transparent)"
|
|
153
|
+
: undefined,
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{env}
|
|
157
|
+
</button>
|
|
158
|
+
);
|
|
159
|
+
})}
|
|
160
|
+
</div>
|
|
161
|
+
) : null}
|
|
162
|
+
|
|
126
163
|
<div className="mono ml-auto flex flex-wrap items-center gap-3">
|
|
127
164
|
{/* The legend IS the filter. Three swatches that explain the three
|
|
128
165
|
dashes and three buttons that turn them off are the same three
|
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
|
+
}
|