@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
|
@@ -10,11 +10,18 @@ import {
|
|
|
10
10
|
technologiesOf,
|
|
11
11
|
} from "../catalog";
|
|
12
12
|
import { flowRoles } from "../lib/derive";
|
|
13
|
+
import { bridges } from "../lib/centrality";
|
|
13
14
|
import { treeHref } from "../lib/source-link";
|
|
14
15
|
import { flowHealth } from "../lib/flow-tree";
|
|
15
16
|
import { adrsForService, isCurrent } from "../lib/adr";
|
|
16
17
|
import { ADR_ROW_COLUMNS, AdrRow } from "../components/AdrRow";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
EVENT_ANCHOR,
|
|
20
|
+
OVERVIEW_ANCHOR,
|
|
21
|
+
SERVICE_ANCHOR,
|
|
22
|
+
aggregatePath,
|
|
23
|
+
paths,
|
|
24
|
+
} from "../routes";
|
|
18
25
|
import { Markdown } from "../components/Markdown";
|
|
19
26
|
import { middleTruncate, plural } from "../lib/format";
|
|
20
27
|
import { Empty, PageHeader, SectionTitle } from "../components/PageHeader";
|
|
@@ -31,6 +38,8 @@ import {
|
|
|
31
38
|
hasAsyncSpec,
|
|
32
39
|
} from "../components/AsyncApiReference";
|
|
33
40
|
import { ChannelRows } from "../components/ChannelRows";
|
|
41
|
+
import { DocsLinks } from "../components/DocsLinks";
|
|
42
|
+
import { DeploymentRows } from "../components/DeploymentRows";
|
|
34
43
|
import { CommandRows } from "../components/CommandRows";
|
|
35
44
|
import { ModuleSpec } from "../components/SourceDoc";
|
|
36
45
|
import { hasSchema, SchemaDocument } from "../components/SchemaDocument";
|
|
@@ -45,7 +54,11 @@ import { NotFound } from "./NotFound";
|
|
|
45
54
|
import { C4View } from "../likec4/C4View";
|
|
46
55
|
import { LevelSwitch } from "../likec4/levels";
|
|
47
56
|
import type { C4Level } from "../likec4/levels";
|
|
48
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
serviceDeployViewId,
|
|
59
|
+
serviceInsideViewId,
|
|
60
|
+
serviceViewId,
|
|
61
|
+
} from "../likec4/ids";
|
|
49
62
|
import { index } from "../data";
|
|
50
63
|
import { storesOfService } from "../lib/data-model";
|
|
51
64
|
import { ErCanvas } from "../er/ErCanvas";
|
|
@@ -112,6 +125,11 @@ export function ServicePage() {
|
|
|
112
125
|
const flows = flowRoles(catalog, service.id);
|
|
113
126
|
const tree = treeHref(service.path, service, allRepos(catalog));
|
|
114
127
|
const owners = ownersOf(service);
|
|
128
|
+
const deployments = index.deploymentsByService.get(service.id) ?? [];
|
|
129
|
+
// The service as a road: the pairs of contexts it stands between, if any.
|
|
130
|
+
// Derived from the whole estate, so it is computed here and not carried
|
|
131
|
+
// on the service, which knows only its own edges.
|
|
132
|
+
const road = bridges(catalog).find((b) => b.id === service.id) ?? null;
|
|
115
133
|
const adrs = adrsForService(catalog, service.id, context.id);
|
|
116
134
|
const current = adrs.filter(isCurrent);
|
|
117
135
|
const retired = adrs.filter((a) => !isCurrent(a));
|
|
@@ -130,6 +148,7 @@ export function ServicePage() {
|
|
|
130
148
|
.find((source) => source !== undefined && hasAsyncSpec(source));
|
|
131
149
|
const showDomain =
|
|
132
150
|
componentKind(service) === "service" || service.aggregates.length > 0;
|
|
151
|
+
const hasModelGroups = service.aggregates.some((aggregate) => aggregate.kind === "model-group");
|
|
133
152
|
const integrations = integrationsFor(service, catalog);
|
|
134
153
|
|
|
135
154
|
const counts: Record<Tab, number | null> = {
|
|
@@ -167,6 +186,17 @@ export function ServicePage() {
|
|
|
167
186
|
right={
|
|
168
187
|
<>
|
|
169
188
|
<span className="chip-lg">{componentKind(service)}</span>
|
|
189
|
+
{road ? (
|
|
190
|
+
<Link
|
|
191
|
+
to={`${paths.overview()}#${OVERVIEW_ANCHOR.bridges}`}
|
|
192
|
+
className="chip-lg border-line text-muted hover:text-ink"
|
|
193
|
+
title={`${Math.round(road.betweenness * 100)}% of the shortest paths between other services run through this one — see the bridges on the overview`}
|
|
194
|
+
>
|
|
195
|
+
bridge
|
|
196
|
+
<span className="text-muted">·</span>
|
|
197
|
+
{road.between.map((pair) => `${pair.a} ↔ ${pair.b}`).join(", ")}
|
|
198
|
+
</Link>
|
|
199
|
+
) : null}
|
|
170
200
|
<ContextPill id={context.id} name={context.name} />
|
|
171
201
|
</>
|
|
172
202
|
}
|
|
@@ -185,6 +215,7 @@ export function ServicePage() {
|
|
|
185
215
|
open ↗
|
|
186
216
|
</a>
|
|
187
217
|
) : null}
|
|
218
|
+
<DocsLinks name={service.name} />
|
|
188
219
|
{/* Who to ask, read out of CODEOWNERS. A handle is copied rather
|
|
189
220
|
than linked: it is what a reviewer types into a request, and a
|
|
190
221
|
link to a team page is a page most readers cannot open. */}
|
|
@@ -219,7 +250,7 @@ export function ServicePage() {
|
|
|
219
250
|
className="rounded-control hover:text-ink"
|
|
220
251
|
>
|
|
221
252
|
<span className="tnum">{service.aggregates.length}</span>{" "}
|
|
222
|
-
{plural(service.aggregates.length, "aggregate")}
|
|
253
|
+
{plural(service.aggregates.length, hasModelGroups ? "group" : "aggregate")}
|
|
223
254
|
</a>
|
|
224
255
|
) : null}
|
|
225
256
|
{showDomain ? (
|
|
@@ -306,13 +337,39 @@ export function ServicePage() {
|
|
|
306
337
|
/>
|
|
307
338
|
</section>
|
|
308
339
|
) : null}
|
|
340
|
+
{/* Where it runs, read from the deployer's snapshot. Beside the
|
|
341
|
+
commands because it answers the next question after "how do
|
|
342
|
+
I build it": which commit stands where. Absent entirely when
|
|
343
|
+
no snapshot places it - a heading over an empty list would
|
|
344
|
+
claim the service runs nowhere, and nobody made that claim. */}
|
|
345
|
+
{deployments.length > 0 ? (
|
|
346
|
+
<section
|
|
347
|
+
id={SERVICE_ANCHOR.deployments}
|
|
348
|
+
className="mt-section max-w-table"
|
|
349
|
+
>
|
|
350
|
+
<SectionTitle anchor={SERVICE_ANCHOR.deployments}>
|
|
351
|
+
Where it runs
|
|
352
|
+
</SectionTitle>
|
|
353
|
+
<DeploymentRows deployments={deployments} />
|
|
354
|
+
{/* The same rows as a picture: the service's instances with
|
|
355
|
+
the environment, cluster and namespace drawn as the
|
|
356
|
+
frames they are. Under the rows, because the rows carry
|
|
357
|
+
the commit and the images and the picture carries where. */}
|
|
358
|
+
<div className="mt-3">
|
|
359
|
+
<C4View
|
|
360
|
+
viewId={serviceDeployViewId(service)}
|
|
361
|
+
height={220}
|
|
362
|
+
/>
|
|
363
|
+
</div>
|
|
364
|
+
</section>
|
|
365
|
+
) : null}
|
|
309
366
|
{showDomain ? (
|
|
310
367
|
<section
|
|
311
368
|
id={SERVICE_ANCHOR.aggregates}
|
|
312
369
|
className="mt-section max-w-table"
|
|
313
370
|
>
|
|
314
371
|
<SectionTitle anchor={SERVICE_ANCHOR.aggregates}>
|
|
315
|
-
Aggregates
|
|
372
|
+
{hasModelGroups ? "Aggregates and model groups" : "Aggregates"}
|
|
316
373
|
</SectionTitle>
|
|
317
374
|
{/* icon, slug, name, event count, actions - one column each,
|
|
318
375
|
so the counts stack up instead of drifting with the name. */}
|
|
@@ -336,7 +393,7 @@ export function ServicePage() {
|
|
|
336
393
|
>
|
|
337
394
|
{aggregate.slug}
|
|
338
395
|
</Link>
|
|
339
|
-
<span className="meta">{aggregate.name}</span>
|
|
396
|
+
<span className="meta">{aggregate.name}{aggregate.kind === "model-group" ? " · model group" : ""}</span>
|
|
340
397
|
<Link
|
|
341
398
|
to={`${to}#bb-events`}
|
|
342
399
|
className="mono rounded-control hover:underline"
|
package/src/pages/Settings.tsx
CHANGED
|
@@ -48,17 +48,26 @@ import {
|
|
|
48
48
|
import type { Discovery, ProjectDraft, ProjectPlan, RunEvent } from "../lib/local-api";
|
|
49
49
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
50
50
|
import { localKeys, localStatusQuery } from "../lib/queries";
|
|
51
|
-
import {
|
|
51
|
+
import { pluginByName, pluginIcon } from "../lib/plugins";
|
|
52
|
+
import { PluginIcon } from "../components/PluginIcon";
|
|
53
|
+
import { bare, sourceHref, treeHref } from "../lib/source-link";
|
|
52
54
|
import { paths } from "../routes";
|
|
53
55
|
import { CapabilityEmpty, Empty, SectionTitle } from "../components/PageHeader";
|
|
54
56
|
import { Modal } from "../components/Overlay";
|
|
55
57
|
import { MachineDocs } from "../components/MachineDocs";
|
|
56
58
|
import { DeliverySettings } from "./settings/DeliverySettings";
|
|
59
|
+
import { RecordingSettings } from "./settings/RecordingSettings";
|
|
60
|
+
import { RulesSettings } from "./settings/RulesSettings";
|
|
57
61
|
import { PreferencesSettings } from "./settings/PreferencesSettings";
|
|
58
62
|
import { AboutSettings } from "./settings/AboutSettings";
|
|
59
63
|
import { IntegrationsSettings } from "./settings/IntegrationsSettings";
|
|
64
|
+
import { DjangoAggregateChoices } from "./settings/DjangoAggregateChoices";
|
|
65
|
+
import { djangoAggregateCandidates, djangoAggregateMessage } from "../lib/django-aggregates";
|
|
60
66
|
import { CatEmptyState, CatIllustration } from "../components/CatIllustration";
|
|
61
67
|
import { CommitLink } from "../components/CommitLink";
|
|
68
|
+
import { groupDiagnostics, warningDiagnostic } from "../lib/warnings";
|
|
69
|
+
import type { WarningDiagnostic, WarningSeverity } from "../lib/warnings";
|
|
70
|
+
import { AnimatePresence, m, rise } from "../lib/motion";
|
|
62
71
|
|
|
63
72
|
type Health = "healthy" | "changed" | "failed" | "unchecked";
|
|
64
73
|
type ProjectSource = ProjectDraft["source"];
|
|
@@ -261,44 +270,89 @@ function PipelineSteps({ steps }: { steps: SetupRunStep[] }) {
|
|
|
261
270
|
if (steps.length === 0) {
|
|
262
271
|
return <p className="mono text-muted">No result recorded for these steps.</p>;
|
|
263
272
|
}
|
|
273
|
+
const warnings = steps.flatMap((step) => step.warnings.map((message) => ({ plugin: step.plugin, message })));
|
|
274
|
+
const diagnostics = steps.flatMap((step) => step.diagnostics.length
|
|
275
|
+
? step.diagnostics
|
|
276
|
+
: step.warnings.map((message) => warningDiagnostic({ plugin: step.plugin, message })));
|
|
264
277
|
return (
|
|
265
|
-
<div className="
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
<div className="
|
|
269
|
-
<
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
<
|
|
274
|
-
|
|
278
|
+
<div className="space-y-2">
|
|
279
|
+
<div className="divide-y divide-line rounded-control border border-line">
|
|
280
|
+
{steps.map((step) => (
|
|
281
|
+
<div key={step.ordinal} className="px-3 py-2">
|
|
282
|
+
<div className="grid gap-2 sm:grid-cols-[5rem_minmax(7rem,1fr)_auto_auto] sm:items-center">
|
|
283
|
+
<span className="mono text-muted">{step.phase}</span>
|
|
284
|
+
<span className="mono truncate text-ink" title={step.plugin}>{step.plugin}</span>
|
|
285
|
+
<span className="mono text-muted">{step.fileCount} {plural(step.fileCount, "file")}</span>
|
|
286
|
+
<div className="flex items-center justify-between gap-2 sm:justify-end">
|
|
287
|
+
<span className="mono text-muted">{duration(step.durationMs)}</span>
|
|
288
|
+
<StepStatus status={step.status} />
|
|
289
|
+
</div>
|
|
275
290
|
</div>
|
|
276
291
|
</div>
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
{warnings.length ? <details open={warnings.length <= 5}>
|
|
295
|
+
<summary className="mono cursor-pointer text-muted">{warnings.length} {plural(warnings.length, "warning")}</summary>
|
|
296
|
+
<div className="mt-2"><WarningPanel diagnostics={diagnostics} compact /></div>
|
|
297
|
+
</details> : null}
|
|
280
298
|
</div>
|
|
281
299
|
);
|
|
282
300
|
}
|
|
283
301
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
302
|
+
function WarningPanel({ diagnostics, compact = false }: { diagnostics: WarningDiagnostic[]; compact?: boolean }) {
|
|
303
|
+
const groups = groupDiagnostics(diagnostics);
|
|
304
|
+
const plugins = [...new Set(groups.map((group) => group.plugin))].sort();
|
|
305
|
+
const rules = [...new Set(groups.map((group) => group.rule))].sort();
|
|
306
|
+
const [plugin, setPlugin] = useState("all");
|
|
307
|
+
const [rule, setRule] = useState("all");
|
|
308
|
+
const [severity, setSeverity] = useState<WarningSeverity | "all">("all");
|
|
309
|
+
const [visibility, setVisibility] = useState<"active" | "suppressed" | "all">("active");
|
|
310
|
+
const shown = groups.filter((group) =>
|
|
311
|
+
(plugin === "all" || group.plugin === plugin)
|
|
312
|
+
&& (rule === "all" || group.rule === rule)
|
|
313
|
+
&& (severity === "all" || group.severity === severity)
|
|
314
|
+
&& (visibility === "all" || (visibility === "suppressed") === group.suppressed),
|
|
315
|
+
);
|
|
316
|
+
const activeCount = groups.filter((group) => !group.suppressed).reduce((sum, group) => sum + group.count, 0);
|
|
317
|
+
const suppressedCount = diagnostics.length - activeCount;
|
|
318
|
+
|
|
289
319
|
return (
|
|
290
|
-
<
|
|
291
|
-
<
|
|
292
|
-
{
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
<
|
|
297
|
-
|
|
298
|
-
</
|
|
299
|
-
|
|
300
|
-
</
|
|
301
|
-
|
|
320
|
+
<div className="space-y-2">
|
|
321
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
322
|
+
<span className="mono text-muted">{activeCount} active · {groups.length} {plural(groups.length, "rule")}</span>
|
|
323
|
+
{suppressedCount ? <span className="chip status-verified">{suppressedCount} suppressed</span> : null}
|
|
324
|
+
<div className="flex flex-wrap gap-2 sm:ml-auto">
|
|
325
|
+
{plugins.length > 1 ? <select aria-label="Filter warnings by plugin" className={`${FIELD} w-auto py-1`} value={plugin} onChange={(event) => setPlugin(event.target.value)}><option value="all">all plugins</option>{plugins.map((name) => <option key={name} value={name}>{name}</option>)}</select> : null}
|
|
326
|
+
{rules.length > 1 ? <select aria-label="Filter warnings by rule" className={`${FIELD} w-auto py-1`} value={rule} onChange={(event) => setRule(event.target.value)}><option value="all">all rules</option>{rules.map((name) => <option key={name} value={name}>{name}</option>)}</select> : null}
|
|
327
|
+
<select aria-label="Filter warnings by severity" className={`${FIELD} w-auto py-1`} value={severity} onChange={(event) => setSeverity(event.target.value as WarningSeverity | "all")}><option value="all">all severities</option><option value="error">error</option><option value="warning">warning</option><option value="info">info</option></select>
|
|
328
|
+
<select aria-label="Filter active or suppressed warnings" className={`${FIELD} w-auto py-1`} value={visibility} onChange={(event) => setVisibility(event.target.value as "active" | "suppressed" | "all")}><option value="active">active</option><option value="suppressed">suppressed</option><option value="all">all</option></select>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
{shown.length ? <div className="space-y-2">{shown.map((group) => {
|
|
332
|
+
const color = group.severity === "error" ? "text-unresolved" : group.severity === "warning" ? "text-declared" : "text-muted";
|
|
333
|
+
const candidates = group.messages.filter((warning) => warning.aggregateCandidates || djangoAggregateCandidates(warning.message));
|
|
334
|
+
const examples = group.messages.filter((warning) => !warning.aggregateCandidates && !djangoAggregateCandidates(warning.message)).slice(0, compact ? 3 : 12);
|
|
335
|
+
return <details key={`${group.plugin}:${group.rule}:${group.suppressed}`} className={`rounded-control border px-3 py-2 ${group.suppressed ? "border-line opacity-75" : group.severity === "error" ? "border-unresolved" : "border-line"}`} open={!compact && shown.length <= 4}>
|
|
336
|
+
<summary className="cursor-pointer list-none">
|
|
337
|
+
<span className="flex flex-wrap items-center gap-2">
|
|
338
|
+
<CircleAlert size={15} className={`shrink-0 ${color}`} />
|
|
339
|
+
<span className="mono text-ink">{group.plugin}</span>
|
|
340
|
+
<span className={`chip ${group.severity === "error" ? "status-unresolved" : group.severity === "warning" ? "status-declared" : ""}`}>{group.severity}</span>
|
|
341
|
+
<span className="mono text-muted">{group.rule}</span>
|
|
342
|
+
<span className="chip ml-auto">×{group.count}</span>
|
|
343
|
+
</span>
|
|
344
|
+
<span className="mt-1 block text-muted"><span className="font-medium text-ink">Next:</span> {group.action}</span>
|
|
345
|
+
{group.suppressionReason ? <span className="mt-1 block text-muted"><span className="font-medium text-ink">Suppressed:</span> {group.suppressionReason}</span> : null}
|
|
346
|
+
</summary>
|
|
347
|
+
<ul className="mono mt-2 space-y-1 text-muted">
|
|
348
|
+
{examples.map((warning, index) => <li key={`${index}:${warning.message}`} className="break-words border-l-2 border-line pl-2">{djangoAggregateMessage(warning.message)}</li>)}
|
|
349
|
+
</ul>
|
|
350
|
+
{candidates.length ? <DjangoAggregateChoices warnings={candidates} /> : null}
|
|
351
|
+
{group.count > examples.length + candidates.length ? <p className="mono mt-2 text-faint">{group.count - examples.length - candidates.length} more occurrences</p> : null}
|
|
352
|
+
</details>;
|
|
353
|
+
})}</div> : <p className="rounded-control border border-line px-3 py-2 text-muted">No warnings match these filters.</p>}
|
|
354
|
+
{!compact && groups.some((group) => !group.suppressed) ? <p className="text-faint">Suppress a reviewed limitation with a typed CEL entry in <span className="mono">portolan.json → warningPolicies</span>. The reason is required and remains visible here after regeneration.</p> : null}
|
|
355
|
+
</div>
|
|
302
356
|
);
|
|
303
357
|
}
|
|
304
358
|
|
|
@@ -307,11 +361,22 @@ function ProjectCard({ project, onRemove }: { project: SetupProject; onRemove?:
|
|
|
307
361
|
const declared = setupInfo.steps.filter((step) => step.projectId === project.id);
|
|
308
362
|
const runSteps = setupInfo.run?.steps.filter((step) => step.projectId === project.id) ?? [];
|
|
309
363
|
const pluginNames = [...new Set(declared.map((step) => step.plugin))];
|
|
364
|
+
// A project rooted at the repository itself owns every source in it.
|
|
365
|
+
const rootedAtRepository = project.root === "." || project.root === "";
|
|
310
366
|
const sources = catalogSources.filter(
|
|
311
|
-
(source) => source.path === project.root || source.path.startsWith(`${project.root}/`),
|
|
367
|
+
(source) => rootedAtRepository || source.path === project.root || source.path.startsWith(`${project.root}/`),
|
|
312
368
|
);
|
|
313
369
|
const outputs = [...new Set(runSteps.flatMap((step) => step.files))];
|
|
370
|
+
// A vendored project's provenance is its pin: the upstream commit the copy
|
|
371
|
+
// was fetched at, which is also what its source links open. The history of
|
|
372
|
+
// THIS checkout says when the copy landed here, which is a different fact.
|
|
373
|
+
const pin = project.repository
|
|
374
|
+
? catalog.repos?.find((held) => bare(held.repo) === bare(project.repository!))
|
|
375
|
+
: undefined;
|
|
314
376
|
const commits = [...new Set(sources.map((source) => source.commit).filter(Boolean))];
|
|
377
|
+
// "uncommitted" is what the history says of a file it does not hold yet;
|
|
378
|
+
// it is not a commit, and a link built from it would open nothing.
|
|
379
|
+
const committed = commits.filter((commit) => commit !== "uncommitted");
|
|
315
380
|
const href = projectHref(project);
|
|
316
381
|
const sourceLink = forge(project);
|
|
317
382
|
const health = healthFor(runSteps, declared.length, setupInfo);
|
|
@@ -342,7 +407,7 @@ function ProjectCard({ project, onRemove }: { project: SetupProject; onRemove?:
|
|
|
342
407
|
<dt>scope</dt><dd className="truncate text-ink">{project.components?.length ? `${project.group ?? project.context} · ${project.components.length} components` : [project.group ?? project.context, project.component ?? project.service].filter(Boolean).join(" · ") || "estate"}</dd>
|
|
343
408
|
<dt>pipeline</dt><dd className="text-ink">{declared.length} {plural(declared.length, "step")}</dd>
|
|
344
409
|
<dt>fragments</dt><dd className="text-ink">{sources.length}</dd>
|
|
345
|
-
<dt>commit</dt><dd className="truncate text-ink" title={commits.join(", ")}>{
|
|
410
|
+
<dt>commit</dt><dd className="truncate text-ink" title={pin ? pin.commit : commits.join(", ")}>{pin ? <CommitLink commit={pin.commit} repository={project.repository} length={12} /> : committed.length === 1 ? <CommitLink commit={committed[0]!} repository={project.repository} length={12} /> : committed.length > 1 ? `${committed.length} source commits` : commits.length > 0 ? "uncommitted" : "not stamped"}</dd>
|
|
346
411
|
</dl>
|
|
347
412
|
|
|
348
413
|
<div className="mt-4 flex flex-wrap gap-1.5" aria-label="Active plugins">
|
|
@@ -457,10 +522,15 @@ function PluginsList() {
|
|
|
457
522
|
const runSteps = setupInfo.run?.steps.filter((step) => step.plugin === plugin.name) ?? [];
|
|
458
523
|
const health = plugin.stepCount === 0 ? "unchecked" : healthFor(runSteps, declared.length, setupInfo);
|
|
459
524
|
const outputs = [...new Set(runSteps.flatMap((step) => step.files))];
|
|
525
|
+
// What the plugin says of itself, when it is one the package ships.
|
|
526
|
+
// A plugin the manifest declares by its own `process` is not on the
|
|
527
|
+
// index, and the row says nothing rather than something made up.
|
|
528
|
+
const shipped = pluginByName(plugin.name);
|
|
460
529
|
return (
|
|
461
530
|
<details key={plugin.name} id={`plugin-${plugin.name}`} className="group scroll-mt-4 border-t border-line">
|
|
462
531
|
<summary className="flex cursor-pointer list-none items-center gap-2.5 px-3 py-1.5 hover:bg-surface/60">
|
|
463
532
|
{health === "healthy" ? <HealthDot health={health} /> : null}
|
|
533
|
+
<PluginIcon icon={pluginIcon(plugin.name, shipped?.category)} className="text-muted" />
|
|
464
534
|
<span className="mono truncate text-ink" title={plugin.name}>{plugin.name}</span>
|
|
465
535
|
<Runtime plugin={plugin} icon />
|
|
466
536
|
{health === "healthy" ? null : <HealthBadge health={health} />}
|
|
@@ -477,6 +547,7 @@ function PluginsList() {
|
|
|
477
547
|
<div className="grid gap-5 lg:grid-cols-[minmax(0,1.4fr)_minmax(14rem,1fr)]">
|
|
478
548
|
<div><div className="label mb-2">last run</div><PipelineSteps steps={runSteps} /></div>
|
|
479
549
|
<div className="space-y-4">
|
|
550
|
+
{shipped ? <div><div className="label mb-2">what it reads</div><p className="text-muted">{shipped.summary}</p><Link to={paths.plugin(plugin.name)} className="mono mt-1 inline-block text-accent hover:underline">options and source →</Link></div> : null}
|
|
480
551
|
<div><div className="label mb-2">runtime</div><p className="mono"><Runtime plugin={plugin} /></p></div>
|
|
481
552
|
<div><div className="label mb-2">used by</div>{plugin.projectIds.length > 0 ? <div className="flex flex-wrap gap-1.5">{plugin.projectIds.map((id) => <Link key={id} to={`${paths.settingsProjects()}#project-${id}`} className="chip border-line-strong hover:border-accent hover:text-accent">{projectNames.get(id) ?? id}</Link>)}</div> : <p className="mono text-muted">{plugin.stepCount > 0 ? "Estate-wide catalog" : "No pipeline step uses this plugin."}</p>}</div>
|
|
482
553
|
{outputs.length > 0 ? <div><div className="label mb-2">outputs</div><ul className="mono space-y-1 text-muted">{outputs.map((output) => <li key={output} className="truncate"><FileLink path={output} /></li>)}</ul></div> : null}
|
|
@@ -581,6 +652,8 @@ const CAPABILITIES: Record<string, { title: string; summary: string }> = {
|
|
|
581
652
|
"rust-domain": { title: "Rust domain model", summary: "Aggregates, entities, value objects and domain events." },
|
|
582
653
|
"java-domain": { title: "Java domain model", summary: "Aggregates, entities, value objects and domain events." },
|
|
583
654
|
"django-domain": { title: "Django data model", summary: "Models and their relationships." },
|
|
655
|
+
"laravel-domain": { title: "Laravel data model", summary: "Eloquent models, events and listeners, routes and controllers." },
|
|
656
|
+
"php-ddd": { title: "PHP DDD model", summary: "Bounded contexts laid out by module and hexagonal layer: aggregates, value objects, domain events, commands, queries, subscribers, Doctrine mappings." },
|
|
584
657
|
openapi: { title: "HTTP API contract", summary: "Operations and messages declared by OpenAPI or Swagger." },
|
|
585
658
|
wsdl: { title: "SOAP contract", summary: "Services, operations and messages declared by WSDL." },
|
|
586
659
|
"http-clients": { title: "Outbound integrations", summary: "HTTP and SOAP calls, provider branches and code flows." },
|
|
@@ -712,7 +785,7 @@ function Wizard({ open, initialSource, onClose, onAdded, onRunStarted }: { open:
|
|
|
712
785
|
function configure(found: Discovery, commit: string, selectedSourcePath: string, projectId?: string) {
|
|
713
786
|
setDiscovery(found);
|
|
714
787
|
const plugins = found.detections.filter((item) => item.selected).map((item) => item.plugin);
|
|
715
|
-
const domainModel = plugins.some((plugin) => ["go-domain", "ts-domain", "rust-domain", "java-domain", "django-domain"].includes(plugin));
|
|
788
|
+
const domainModel = plugins.some((plugin) => ["go-domain", "ts-domain", "rust-domain", "java-domain", "django-domain", "laravel-domain", "php-ddd"].includes(plugin));
|
|
716
789
|
setDraft({ source, root: found.root, repository, ref, commit, sourcePath: selectedSourcePath, ...found.defaults, ...(projectId ? { id: projectId } : {}), contextName: found.defaults.group ? found.defaults.group.replace(/(^|-)([a-z])/g, (_, gap, letter) => `${gap ? " " : ""}${letter.toUpperCase()}`) : "", contextSummary: "", classification: "supporting", groupKind: domainModel ? "bounded-context" : "system", componentKind: domainModel ? "service" : "application", replaceStarter: Boolean(starter), plugins });
|
|
717
790
|
setStage("configure");
|
|
718
791
|
}
|
|
@@ -829,11 +902,15 @@ function Wizard({ open, initialSource, onClose, onAdded, onRunStarted }: { open:
|
|
|
829
902
|
const trialRunning = stage === "trial" && !finished;
|
|
830
903
|
const confirmedDeployables = discovery?.deployables.filter((candidate) => candidate.confidence === "high") ?? [];
|
|
831
904
|
const splitsDeployables = confirmedDeployables.length > 1 && draft?.component === discovery?.defaults.component;
|
|
905
|
+
const trialDiagnostics = trial?.diagnostics ?? [];
|
|
906
|
+
const trialActiveWarnings = trial
|
|
907
|
+
? groupDiagnostics(trialDiagnostics).filter((group) => !group.suppressed).reduce((sum, group) => sum + group.count, 0)
|
|
908
|
+
: 0;
|
|
832
909
|
const quality = trial && draft ? [
|
|
833
910
|
{ label: "Stable project identity", done: Boolean(draft.id.trim() && draft.name.trim()) },
|
|
834
911
|
{ label: "Architecture placement", done: Boolean(draft.group.trim() && (splitsDeployables || draft.component.trim())) },
|
|
835
912
|
{ label: "Catalog facts extracted", done: trial.facts.some((fact) => fact.count > 0) },
|
|
836
|
-
{ label: "Extraction without warnings", done:
|
|
913
|
+
{ label: "Extraction without active warnings", done: trialActiveWarnings === 0 },
|
|
837
914
|
] : [];
|
|
838
915
|
const totalSteps = pipeline?.stepCount ?? plan?.steps.length ?? 0;
|
|
839
916
|
const percent = totalSteps ? Math.round((completedSteps.length / totalSteps) * 100) : 0;
|
|
@@ -877,7 +954,7 @@ function Wizard({ open, initialSource, onClose, onAdded, onRunStarted }: { open:
|
|
|
877
954
|
{discovery.detections.length ? <div className="grid gap-2">{discovery.detections.map((item) => {
|
|
878
955
|
const checked = draft.plugins.includes(item.plugin);
|
|
879
956
|
const capability = CAPABILITIES[item.plugin] ?? { title: item.plugin, summary: "Catalog facts extracted from project files." };
|
|
880
|
-
return <label key={item.plugin} className={`flex cursor-pointer items-start gap-3 rounded-control border px-3 py-3 transition-colors ${checked ? "border-accent bg-surface" : "border-line hover:bg-surface"}`}><input className="mt-1" type="checkbox" checked={checked} onChange={() => setDraft({ ...draft, plugins: checked ? draft.plugins.filter((name) => name !== item.plugin) : [...draft.plugins, item.plugin] })} /><span className="min-w-0 flex-1"><span className="flex flex-wrap items-center gap-2"><span className="font-medium text-ink">{capability.title}</span><span className={`chip ${item.confidence === "high" ? "status-verified" : "status-declared"}`}>{item.confidence} confidence</span>{item.candidates.length > 1 ? <span className="chip status-declared">{item.candidates.length} candidates</span> : null}</span><span className="mt-0.5 block text-muted">{capability.summary}</span><span className="mono mt-1 block truncate text-faint" title={item.evidence}>evidence · {item.evidence}</span><span className="mono mt-0.5 block text-faint">extractor · {item.plugin}</span></span></label>;
|
|
957
|
+
return <label key={item.plugin} className={`flex cursor-pointer items-start gap-3 rounded-control border px-3 py-3 transition-colors ${checked ? "border-accent bg-surface" : "border-line hover:bg-surface"}`}><input aria-label={`Toggle ${capability.title}`} className="mt-1" type="checkbox" checked={checked} onChange={() => setDraft({ ...draft, plugins: checked ? draft.plugins.filter((name) => name !== item.plugin) : [...draft.plugins, item.plugin] })} /><span className="min-w-0 flex-1"><span className="flex flex-wrap items-center gap-2"><span className="font-medium text-ink">{capability.title}</span><span className={`chip ${item.confidence === "high" ? "status-verified" : "status-declared"}`}>{item.confidence} confidence</span>{item.candidates.length > 1 ? <span className="chip status-declared">{item.candidates.length} candidates</span> : null}</span><span className="mt-0.5 block text-muted">{capability.summary}</span><span className="mono mt-1 block truncate text-faint" title={item.evidence}>evidence · {item.evidence}</span>{item.preview?.length ? <span className="mt-2 grid gap-1.5">{item.preview.slice(0, 3).map((preview) => <span key={preview.file} className="block rounded-control border border-line bg-canvas px-2 py-1.5"><span className="mono block truncate text-faint" title={preview.file}>{preview.file}</span><span className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-muted">{Object.entries(preview.fields).map(([field, value]) => <span key={field}><span className="text-faint">{field}</span> · <span className="text-ink">{value}</span></span>)}</span></span>)}{item.preview.length > 3 ? <span className="text-faint">+ {item.preview.length - 3} more recognized records</span> : null}</span> : null}<span className="mono mt-0.5 block text-faint">extractor · {item.plugin}</span></span></label>;
|
|
881
958
|
})}</div> : <Empty>no supported project signals found</Empty>}
|
|
882
959
|
</div>
|
|
883
960
|
</div>
|
|
@@ -895,7 +972,7 @@ function Wizard({ open, initialSource, onClose, onAdded, onRunStarted }: { open:
|
|
|
895
972
|
{trial ? <section><div className="label mb-2">what Portolan found</div>{trial.facts.length ? <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">{trial.facts.map((fact) => <div key={fact.key} className="rounded-control border border-line bg-canvas px-3 py-2"><div className="tnum text-lg font-semibold text-ink">{fact.count}</div><div className="text-muted">{fact.label}</div></div>)}</div> : <div className="rounded-control border border-line bg-surface px-3 py-3 text-muted">Only component metadata was found. You can add APIs, domain models and stores later without re-adding the project.</div>}</section> : null}
|
|
896
973
|
{trial ? <section><div className="label mb-2">catalog readiness</div><div className="grid gap-2 sm:grid-cols-2">{quality.map((item) => <div key={item.label} className="flex items-center gap-2 rounded-control border border-line px-3 py-2"><span className={`flex size-5 items-center justify-center rounded-full ${item.done ? "bg-verified text-canvas" : "bg-surface text-declared"}`}>{item.done ? <Check size={12} aria-hidden /> : <CircleAlert size={12} aria-hidden />}</span><span className={item.done ? "text-ink" : "text-muted"}>{item.label}</span></div>)}</div></section> : null}
|
|
897
974
|
<section><div className="label mb-2">extractor results</div><div className="divide-y divide-line rounded-control border border-line">{(trial?.steps ?? completedSteps).map((step) => <div key={`${step.plugin}:${"ordinal" in step ? step.ordinal : "trial"}`} className="grid gap-1 px-3 py-2 sm:grid-cols-[1fr_auto_auto]"><span><span className="font-medium text-ink">{CAPABILITIES[step.plugin]?.title ?? step.plugin}</span><span className="mono ml-2 text-faint">{step.plugin}</span>{step.message ? <span className="mt-1 block text-unresolved">{step.message}</span> : null}</span><span className="mono text-muted">{step.fileCount} {plural(step.fileCount, "file")}</span><span className={`chip ${step.status === "failed" ? "status-unresolved" : "status-verified"}`}>{step.status === "failed" ? "failed" : "read"}</span></div>)}</div></section>
|
|
898
|
-
{
|
|
975
|
+
{trialDiagnostics.length ? <section><div className="label mb-2">diagnostics · {trialDiagnostics.length}</div><WarningPanel diagnostics={trialDiagnostics} /></section> : null}
|
|
899
976
|
{finished?.status === "failed" && logs.length ? <details><summary className="cursor-pointer text-muted">Generator log · {logs.length} lines</summary><pre className="mono mt-2 max-h-48 overflow-auto whitespace-pre-wrap rounded-control bg-surface p-3 text-muted">{logs.map((event) => event.message).join("\n")}</pre></details> : null}
|
|
900
977
|
{trial ? <div><div className="label mb-2">changes after apply</div><dl className="mono grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-2 text-muted"><dt>project</dt><dd className="text-ink">{plan.project.id}</dd>{plan.fetch ? <><dt>pin</dt><dd className="truncate text-ink"><CommitLink commit={plan.fetch.commit} repository={plan.fetch.repo} length={12} /></dd></> : null}<dt>source</dt><dd className="truncate text-ink">{plan.source}</dd><dt>pipeline</dt><dd className="text-ink">{plan.steps.length + (plan.fetch ? 1 : 0)} {plural(plan.steps.length + (plan.fetch ? 1 : 0), "extract step")}</dd><dt>fragments</dt><dd className="text-ink">{trial.generatedFiles} generated</dd></dl></div> : null}
|
|
901
978
|
</div>
|
|
@@ -955,11 +1032,23 @@ const SETTINGS_LINKS = [
|
|
|
955
1032
|
["Projects", paths.settingsProjects()],
|
|
956
1033
|
["Pipeline", paths.settingsPipeline()],
|
|
957
1034
|
["Delivery", paths.settingsDelivery()],
|
|
1035
|
+
["Recordings", paths.settingsRecordings()],
|
|
1036
|
+
["Rules", paths.settingsRules()],
|
|
958
1037
|
["Integrations", paths.settingsIntegrations()],
|
|
959
1038
|
["Preferences", paths.settingsPreferences()],
|
|
960
1039
|
["About", paths.settingsAbout()],
|
|
961
1040
|
] as const;
|
|
962
1041
|
|
|
1042
|
+
function RecordingsRoute({ local }: { local: boolean }) {
|
|
1043
|
+
const setupInfo = useSetup();
|
|
1044
|
+
return (
|
|
1045
|
+
<section>
|
|
1046
|
+
<SectionTitle right={local ? "reviewed before writing" : "local mode required"}>Recordings</SectionTitle>
|
|
1047
|
+
<RecordingSettings local={local} projects={setupInfo.projects} />
|
|
1048
|
+
</section>
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
963
1052
|
function SettingsNav() {
|
|
964
1053
|
return (
|
|
965
1054
|
<nav className="mt-5 border-b border-line" aria-label="Settings sections">
|
|
@@ -1094,7 +1183,7 @@ function PipelineSettings() {
|
|
|
1094
1183
|
) : (
|
|
1095
1184
|
<>
|
|
1096
1185
|
<PluginsList />
|
|
1097
|
-
<p className="mono mt-2 text-muted">WASM runs without network or environment access, and a generator without a filesystem. A host process runs with the permissions of the build; a plugin in the host is Portolan's own code doing what needs a socket, such as fetching another repository
|
|
1186
|
+
<p className="mono mt-2 text-muted">WASM runs without network or environment access, and a generator without a filesystem. A host process runs with the permissions of the build; a plugin in the host is Portolan's own code doing what needs a socket, such as fetching another repository. Every plugin the package ships, with what it reads and the options it takes, is on the <Link to={paths.plugins()} className="text-accent hover:underline">plugin reference</Link>.</p>
|
|
1098
1187
|
</>
|
|
1099
1188
|
)}
|
|
1100
1189
|
</section>
|
|
@@ -1136,6 +1225,8 @@ function SettingsContent({ local, onAdd, onRemove, onGenerate }: { local: boolea
|
|
|
1136
1225
|
<Route path="projects" element={<ProjectsSettings local={local} onAdd={onAdd} onRemove={onRemove} />} />
|
|
1137
1226
|
<Route path="pipeline" element={<PipelineSettings />} />
|
|
1138
1227
|
<Route path="delivery" element={<section><SectionTitle right={local ? "preview before writing" : "local mode required"}>Delivery presets</SectionTitle><DeliverySettings local={local} /></section>} />
|
|
1228
|
+
<Route path="recordings" element={<RecordingsRoute local={local} />} />
|
|
1229
|
+
<Route path="rules" element={<RulesSettings local={local} />} />
|
|
1139
1230
|
<Route path="integrations" element={<IntegrationsSettings />} />
|
|
1140
1231
|
<Route path="preferences" element={<PreferencesSettings />} />
|
|
1141
1232
|
<Route path="about" element={<AboutSettings />} />
|
|
@@ -1148,6 +1239,88 @@ function SettingsContent({ local, onAdd, onRemove, onGenerate }: { local: boolea
|
|
|
1148
1239
|
);
|
|
1149
1240
|
}
|
|
1150
1241
|
|
|
1242
|
+
function RemoveProjectDialog({
|
|
1243
|
+
project,
|
|
1244
|
+
busy,
|
|
1245
|
+
onClose,
|
|
1246
|
+
onConfirm,
|
|
1247
|
+
}: {
|
|
1248
|
+
project: SetupProject | null;
|
|
1249
|
+
busy: boolean;
|
|
1250
|
+
onClose: () => void;
|
|
1251
|
+
onConfirm: () => void;
|
|
1252
|
+
}) {
|
|
1253
|
+
const name = project?.name ?? "project";
|
|
1254
|
+
return (
|
|
1255
|
+
<Modal
|
|
1256
|
+
open={project !== null}
|
|
1257
|
+
onClose={busy ? () => {} : onClose}
|
|
1258
|
+
label={`Remove ${name}`}
|
|
1259
|
+
width="min(520px,92vw)"
|
|
1260
|
+
>
|
|
1261
|
+
<div className="flex items-center gap-3 border-b border-line px-5 py-4">
|
|
1262
|
+
<m.span
|
|
1263
|
+
animate={busy ? { rotate: 360 } : { rotate: 0 }}
|
|
1264
|
+
transition={busy ? { duration: 0.9, ease: "linear", repeat: Infinity } : undefined}
|
|
1265
|
+
className="flex size-8 shrink-0 items-center justify-center rounded-full bg-unresolved/10 text-unresolved"
|
|
1266
|
+
>
|
|
1267
|
+
{busy ? <LoaderCircle size={17} aria-hidden /> : <Trash2 size={16} aria-hidden />}
|
|
1268
|
+
</m.span>
|
|
1269
|
+
<div className="min-w-0">
|
|
1270
|
+
<div className="font-semibold text-ink">{busy ? `Removing ${name}` : `Remove ${name}?`}</div>
|
|
1271
|
+
<div className="mono mt-0.5 truncate text-faint">{project?.id}</div>
|
|
1272
|
+
</div>
|
|
1273
|
+
</div>
|
|
1274
|
+
|
|
1275
|
+
<div className="min-h-40 p-5">
|
|
1276
|
+
<AnimatePresence mode="wait" initial={false}>
|
|
1277
|
+
{busy ? (
|
|
1278
|
+
<m.div key="removing" {...rise} aria-live="polite">
|
|
1279
|
+
<p className="font-medium text-ink">Updating project configuration…</p>
|
|
1280
|
+
<p className="mt-2 text-muted">
|
|
1281
|
+
Portolan is removing the project and its extraction steps. The source tree remains untouched.
|
|
1282
|
+
</p>
|
|
1283
|
+
<div
|
|
1284
|
+
role="progressbar"
|
|
1285
|
+
aria-label={`Removing ${name}`}
|
|
1286
|
+
className="mt-5 h-1.5 overflow-hidden rounded-full bg-surface"
|
|
1287
|
+
>
|
|
1288
|
+
<m.div
|
|
1289
|
+
className="h-full w-2/5 rounded-full bg-unresolved"
|
|
1290
|
+
initial={{ x: "-100%" }}
|
|
1291
|
+
animate={{ x: ["-100%", "250%"] }}
|
|
1292
|
+
transition={{ duration: 1.1, ease: "easeInOut", repeat: Infinity }}
|
|
1293
|
+
/>
|
|
1294
|
+
</div>
|
|
1295
|
+
</m.div>
|
|
1296
|
+
) : (
|
|
1297
|
+
<m.div key="confirm" {...rise}>
|
|
1298
|
+
<p className="text-muted">
|
|
1299
|
+
This removes the project, its extraction steps and its dedicated catalog configuration from <span className="mono text-ink">portolan.json</span>. Your source code stays untouched.
|
|
1300
|
+
</p>
|
|
1301
|
+
<p className="mt-3 text-muted">
|
|
1302
|
+
You can undo immediately. Generated documentation changes remain pending until you preview and apply them.
|
|
1303
|
+
</p>
|
|
1304
|
+
</m.div>
|
|
1305
|
+
)}
|
|
1306
|
+
</AnimatePresence>
|
|
1307
|
+
</div>
|
|
1308
|
+
|
|
1309
|
+
<div className="flex justify-end gap-2 border-t border-line px-5 py-4">
|
|
1310
|
+
<button type="button" className="tbtn" onClick={onClose} disabled={busy}>Cancel</button>
|
|
1311
|
+
<button type="button" className="product-primary product-danger min-w-36" onClick={onConfirm} disabled={busy}>
|
|
1312
|
+
<AnimatePresence mode="wait" initial={false}>
|
|
1313
|
+
<m.span key={busy ? "busy" : "ready"} {...rise} className="flex items-center justify-center gap-2">
|
|
1314
|
+
{busy ? <LoaderCircle size={15} aria-hidden /> : <Trash2 size={15} aria-hidden />}
|
|
1315
|
+
{busy ? "Removing…" : "Remove project"}
|
|
1316
|
+
</m.span>
|
|
1317
|
+
</AnimatePresence>
|
|
1318
|
+
</button>
|
|
1319
|
+
</div>
|
|
1320
|
+
</Modal>
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1151
1324
|
export function Settings() {
|
|
1152
1325
|
useDocumentTitle("Settings");
|
|
1153
1326
|
const queryClient = useQueryClient();
|
|
@@ -1210,11 +1383,12 @@ export function Settings() {
|
|
|
1210
1383
|
<SetupContext.Provider value={setup}>
|
|
1211
1384
|
<SettingsContent local={local} onAdd={setWizardSource} onRemove={setRemoveTarget} onGenerate={() => void generate()} />
|
|
1212
1385
|
<Wizard open={wizardSource !== null} initialSource={wizardSource ?? "local"} onClose={() => setWizardSource(null)} onAdded={setSetup} onRunStarted={(id) => { setRunId(id); setRunOpen(true); }} />
|
|
1213
|
-
<
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1386
|
+
<RemoveProjectDialog
|
|
1387
|
+
project={removeTarget}
|
|
1388
|
+
busy={removeBusy}
|
|
1389
|
+
onClose={() => setRemoveTarget(null)}
|
|
1390
|
+
onConfirm={() => void confirmRemove()}
|
|
1391
|
+
/>
|
|
1218
1392
|
<RunDialog runId={runId} open={runOpen} onClose={() => setRunOpen(false)} onFinished={refresh} onApply={(preview) => void generate(preview)} />
|
|
1219
1393
|
</SetupContext.Provider>
|
|
1220
1394
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArrowRight, BookOpen, CircleDot, GitBranch, ShieldCheck } from "lucide-react";
|
|
1
|
+
import { ArrowRight, BookOpen, CircleDot, GitBranch, Puzzle, ShieldCheck } from "lucide-react";
|
|
2
2
|
import type { Variants } from "motion/react";
|
|
3
3
|
import { Link } from "react-router";
|
|
4
4
|
|
|
@@ -94,6 +94,13 @@ export function AboutSettings() {
|
|
|
94
94
|
>
|
|
95
95
|
<div className="label mb-2">resources</div>
|
|
96
96
|
<div className="divide-y divide-line">
|
|
97
|
+
<Link
|
|
98
|
+
to={paths.plugins()}
|
|
99
|
+
className="flex items-center gap-2 py-2.5 text-muted transition-colors hover:text-ink"
|
|
100
|
+
>
|
|
101
|
+
<Puzzle size={15} aria-hidden className="shrink-0" />
|
|
102
|
+
Plugin reference
|
|
103
|
+
</Link>
|
|
97
104
|
<a
|
|
98
105
|
href={PRODUCT_README}
|
|
99
106
|
target="_blank"
|