@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
package/src/merge.test.ts
CHANGED
|
@@ -858,6 +858,88 @@ describe("a second source that has seen the flow run", () => {
|
|
|
858
858
|
expect(first?.type === "step" ? first.status : "").toBe("declared");
|
|
859
859
|
});
|
|
860
860
|
|
|
861
|
+
it("takes a hop a recording showed that the code does not declare, where the recording put it", () => {
|
|
862
|
+
const seen = {
|
|
863
|
+
type: "step" as const,
|
|
864
|
+
id: "seen1",
|
|
865
|
+
from: "auth.auth",
|
|
866
|
+
to: "profile",
|
|
867
|
+
kind: "rpc" as const,
|
|
868
|
+
label: "GET /v1/profiles/42",
|
|
869
|
+
status: "unresolved" as const,
|
|
870
|
+
seen: { traces: 2 },
|
|
871
|
+
};
|
|
872
|
+
const lane = { id: "profile", kind: "unknown" as const, context: null, label: "profile" };
|
|
873
|
+
const example = { id: "telemetry/traces.jsonl#t1", recording: "telemetry/traces.jsonl", traceId: "t1", durationMs: 2.5, steps: [{ step: "s1", durationMs: 2.5 }] };
|
|
874
|
+
const merged = mergeCatalogs([
|
|
875
|
+
source("a.json", { flows: [flow([step("s1", "declared"), step("s2", "declared")])] }),
|
|
876
|
+
source("b.json", {
|
|
877
|
+
flows: [{
|
|
878
|
+
...flow([{ ...step("s1", "verified"), seen: { traces: 2 } }, seen, step("s2", "declared")]),
|
|
879
|
+
participants: [...flow([]).participants, lane],
|
|
880
|
+
examples: [example],
|
|
881
|
+
}],
|
|
882
|
+
}),
|
|
883
|
+
]);
|
|
884
|
+
|
|
885
|
+
expect(merged.conflicts).toEqual([]);
|
|
886
|
+
const out = merged.catalog.flows[0]!;
|
|
887
|
+
expect(out.steps.map((s) => (s.type === "step" ? `${s.id}:${s.status}` : s.type))).toEqual(["s1:verified", "seen1:unresolved", "s2:declared"]);
|
|
888
|
+
expect(out.steps[0]?.type === "step" ? out.steps[0].seen : undefined).toEqual({ traces: 2 });
|
|
889
|
+
expect(out.participants.map((p) => p.id)).toEqual(["client", "auth.auth", "profile"]);
|
|
890
|
+
expect(out.examples).toEqual([example]);
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it("takes a frame where the recordings parted, and not a frame the code did not write", () => {
|
|
894
|
+
const seenStep = (id: string, label: string) => ({ ...step(id, "verified" as const), label, seen: { traces: 1 } });
|
|
895
|
+
const frame = {
|
|
896
|
+
type: "alt" as const,
|
|
897
|
+
id: "seen-alt1",
|
|
898
|
+
branches: [
|
|
899
|
+
{ title: "getUser", steps: [seenStep("seen1", "getUser")], seen: { traces: 1 } },
|
|
900
|
+
{ title: "otherwise", steps: [], seen: { traces: 1 } },
|
|
901
|
+
],
|
|
902
|
+
};
|
|
903
|
+
const merged = mergeCatalogs([
|
|
904
|
+
source("a.json", { flows: [flow([step("s1", "declared"), step("s2", "declared")])] }),
|
|
905
|
+
source("b.json", { flows: [flow([step("s1", "verified"), frame, step("s2", "declared")])] }),
|
|
906
|
+
]);
|
|
907
|
+
expect(merged.conflicts).toEqual([]);
|
|
908
|
+
expect(merged.catalog.flows[0]?.steps.map((s) => s.id)).toEqual(["s1", "seen-alt1", "s2"]);
|
|
909
|
+
|
|
910
|
+
const unseen = { ...frame, branches: [{ title: "x", steps: [step("x1", "verified")] }] };
|
|
911
|
+
const refused = mergeCatalogs([
|
|
912
|
+
source("a.json", { flows: [flow([step("s1", "declared")])] }),
|
|
913
|
+
source("b.json", { flows: [flow([step("s1", "verified"), unseen])] }),
|
|
914
|
+
]);
|
|
915
|
+
expect(refused.conflicts).toHaveLength(1);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
it("is still a conflict when a lane the code declared is missing or a declared step is", () => {
|
|
919
|
+
const fewerLanes = mergeCatalogs([
|
|
920
|
+
source("a.json", { flows: [flow([step("s1", "declared")])] }),
|
|
921
|
+
source("b.json", { flows: [{ ...flow([step("s1", "verified")]), participants: [{ id: "client", kind: "actor" as const, context: null }] }] }),
|
|
922
|
+
]);
|
|
923
|
+
expect(fewerLanes.conflicts).toHaveLength(1);
|
|
924
|
+
|
|
925
|
+
const fewerSteps = mergeCatalogs([
|
|
926
|
+
source("a.json", { flows: [flow([step("s1", "declared"), step("s2", "declared")])] }),
|
|
927
|
+
source("b.json", { flows: [flow([step("s1", "verified")])] }),
|
|
928
|
+
]);
|
|
929
|
+
expect(fewerSteps.conflicts).toHaveLength(1);
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
it("keeps an example it already has when a second recording says it again", () => {
|
|
933
|
+
const example = (id: string) => ({ id, recording: "t.jsonl", traceId: id, durationMs: 1, steps: [] });
|
|
934
|
+
const merged = mergeCatalogs([
|
|
935
|
+
source("a.json", { flows: [{ ...flow([step("s1", "declared")]), examples: [example("a")] }] }),
|
|
936
|
+
source("b.json", { flows: [{ ...flow([step("s1", "verified")]), examples: [example("a"), example("b")] }] }),
|
|
937
|
+
]);
|
|
938
|
+
|
|
939
|
+
expect(merged.conflicts).toEqual([]);
|
|
940
|
+
expect(merged.catalog.flows[0]?.examples?.map((e) => e.id)).toEqual(["a", "b"]);
|
|
941
|
+
});
|
|
942
|
+
|
|
861
943
|
it("lets verified win for a consumer and a call declared twice", () => {
|
|
862
944
|
const withEdges = (status: "declared" | "verified") => {
|
|
863
945
|
const ctx = context("shop", ["shop.oms"]);
|
package/src/merge.ts
CHANGED
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
// context's name is a fact about the estate that belongs on the Problems
|
|
16
16
|
// page, next to the other things that are true and unfortunate.
|
|
17
17
|
|
|
18
|
+
import { deploymentBasis } from "./catalog-model.ts";
|
|
18
19
|
import type {
|
|
19
20
|
Aggregate,
|
|
21
|
+
Deployment,
|
|
22
|
+
DeploymentBasis,
|
|
23
|
+
DeploymentDrift,
|
|
20
24
|
Alt,
|
|
21
25
|
AltBranch,
|
|
22
26
|
Block,
|
|
@@ -42,13 +46,14 @@ import type {
|
|
|
42
46
|
* A catalog as a FILE may carry it, which is not quite a catalog as a reader
|
|
43
47
|
* gets one: the two stamps are optional here and required downstream.
|
|
44
48
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* A fragment carries no stamp of its own (portolan.0010). When it last
|
|
50
|
+
* changed, and in which commit, is what the history of the checkout says,
|
|
51
|
+
* and whoever reads the file asks the history and hands the answer over
|
|
52
|
+
* beside it. A file may still say something - a fragment written before
|
|
53
|
+
* portolan.0010 does - and that is taken only when nobody read the history.
|
|
54
|
+
* A provenance typed in by hand is a claim no process keeps true, so a source
|
|
55
|
+
* that says nothing and was read outside any history is stamped by nobody,
|
|
56
|
+
* and saying nothing is treated as what it is - an absence of evidence, which
|
|
52
57
|
* drops out of the merge rather than voting in it.
|
|
53
58
|
*/
|
|
54
59
|
export type SourceCatalog = Omit<Catalog, "generatedAt" | "commit"> &
|
|
@@ -59,6 +64,12 @@ export interface CatalogSource {
|
|
|
59
64
|
/** Where it was read from, as a reader would type it. */
|
|
60
65
|
path: string;
|
|
61
66
|
catalog: SourceCatalog;
|
|
67
|
+
/**
|
|
68
|
+
* When the file last changed and in which commit, as the reader found out
|
|
69
|
+
* from the history of the checkout (portolan.0010). Left out when there
|
|
70
|
+
* was no history to ask, and the file then speaks for itself.
|
|
71
|
+
*/
|
|
72
|
+
stamp?: Pick<SourceStamp, "generatedAt" | "commit">;
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
/**
|
|
@@ -114,14 +125,16 @@ export function mergeCatalogs(sources: CatalogSource[]): MergeResult {
|
|
|
114
125
|
const terms: NonNullable<Catalog["terms"]> = [];
|
|
115
126
|
const repos: NonNullable<Catalog["repos"]> = [];
|
|
116
127
|
const repoOrigin = new Map<string, string>();
|
|
128
|
+
const deployments: NonNullable<Catalog["deployments"]> = [];
|
|
129
|
+
const deploymentOrigin = new Map<string, Map<DeploymentBasis, string>>(); // id -> basis -> source path
|
|
117
130
|
const externals = new Map<string, External>();
|
|
118
131
|
const externalOrigin = new Map<string, string>();
|
|
119
132
|
const seen = new Map<string, string>(); // flow/adr/store/module/term id -> source path
|
|
120
133
|
|
|
121
134
|
const stamps: SourceStamp[] = ordered.map((source) => ({
|
|
122
135
|
path: source.path,
|
|
123
|
-
generatedAt: source.catalog.generatedAt ?? "",
|
|
124
|
-
commit: source.catalog.commit ?? "",
|
|
136
|
+
generatedAt: source.stamp?.generatedAt ?? source.catalog.generatedAt ?? "",
|
|
137
|
+
commit: source.stamp?.commit ?? source.catalog.commit ?? "",
|
|
125
138
|
}));
|
|
126
139
|
|
|
127
140
|
for (const { path, catalog } of ordered) {
|
|
@@ -242,6 +255,15 @@ export function mergeCatalogs(sources: CatalogSource[]): MergeResult {
|
|
|
242
255
|
for (const store of catalog.stores ?? []) {
|
|
243
256
|
if (claim(seen, store.id, path, conflicts, "store")) stores.push(store);
|
|
244
257
|
}
|
|
258
|
+
// Two sources may speak about one Application, and are meant to: the
|
|
259
|
+
// GitOps tree says what should run, the deployer says what does, and
|
|
260
|
+
// the row a reader sees is the two laid over each other with the
|
|
261
|
+
// difference kept as drift (portolan.0013). Two rows of the SAME basis
|
|
262
|
+
// for one id is the collision - two snapshots of one server, two trees
|
|
263
|
+
// declaring one name - and is reported like any other.
|
|
264
|
+
for (const deployment of catalog.deployments ?? []) {
|
|
265
|
+
foldDeployment(deployments, deploymentOrigin, deployment, path, conflicts);
|
|
266
|
+
}
|
|
245
267
|
// Claimed by id like everything else at this level, and that is exactly why
|
|
246
268
|
// a module's id is its registry-global name rather than one derived from an
|
|
247
269
|
// owner: the producer and each consumer describe the same module from
|
|
@@ -317,6 +339,7 @@ export function mergeCatalogs(sources: CatalogSource[]): MergeResult {
|
|
|
317
339
|
if (modules.length > 0) merged.modules = modules;
|
|
318
340
|
if (terms.length > 0) merged.terms = terms;
|
|
319
341
|
if (repos.length > 0) merged.repos = repos;
|
|
342
|
+
if (deployments.length > 0) merged.deployments = deployments;
|
|
320
343
|
if (externals.size > 0) merged.externals = [...externals.values()];
|
|
321
344
|
|
|
322
345
|
return { catalog: merged, sources: stamps, conflicts };
|
|
@@ -492,6 +515,8 @@ function mergeService(
|
|
|
492
515
|
...(incoming.commands
|
|
493
516
|
? { commands: incoming.commands.map((c) => ({ ...c })) }
|
|
494
517
|
: {}),
|
|
518
|
+
...(incoming.hosts ? { hosts: [...incoming.hosts] } : {}),
|
|
519
|
+
...(incoming.dials ? { dials: [...incoming.dials] } : {}),
|
|
495
520
|
});
|
|
496
521
|
origin.set(incoming.id, path);
|
|
497
522
|
|
|
@@ -585,6 +610,18 @@ function mergeService(
|
|
|
585
610
|
existing.commands = commands;
|
|
586
611
|
}
|
|
587
612
|
|
|
613
|
+
// Unioned like owners: two manifest trees naming where a service answers,
|
|
614
|
+
// or what it dials, are two deployments of it, and the page shows both.
|
|
615
|
+
for (const field of ["hosts", "dials"] as const) {
|
|
616
|
+
const theirs = incoming[field];
|
|
617
|
+
if (!theirs?.length) continue;
|
|
618
|
+
const names = existing[field] ?? [];
|
|
619
|
+
for (const name of theirs) {
|
|
620
|
+
if (!names.includes(name)) names.push(name);
|
|
621
|
+
}
|
|
622
|
+
existing[field] = names;
|
|
623
|
+
}
|
|
624
|
+
|
|
588
625
|
if (incoming.channels?.length) {
|
|
589
626
|
// Keyed by address, because that is the whole of a channel's identity. Two
|
|
590
627
|
// documents describing one channel is not a conflict worth reporting - a
|
|
@@ -923,26 +960,84 @@ function raise<T extends { status: Status; note?: string }>(
|
|
|
923
960
|
*
|
|
924
961
|
* Only `declared` is raised. `unresolved` means the far end is not in the
|
|
925
962
|
* catalog, and a trace showing the hop does not put it there.
|
|
963
|
+
*
|
|
964
|
+
* The second declaration may also say more than the first: a hop it saw
|
|
965
|
+
* that the code does not declare, carried as a step with `seen` on it and
|
|
966
|
+
* an id the code never gave, sitting where the recording put it; a lane
|
|
967
|
+
* such a hop needed, added after the declared ones; and the recordings
|
|
968
|
+
* themselves, as examples. Each is taken as it comes - a recording is the
|
|
969
|
+
* one witness for it - and nothing already declared moves.
|
|
926
970
|
*/
|
|
927
971
|
export function overlayFlow(existing: Flow, incoming: Flow): Flow | undefined {
|
|
928
|
-
const
|
|
929
|
-
|
|
972
|
+
const mine = existing.participants.map((p) => p.id);
|
|
973
|
+
const theirs = incoming.participants.map((p) => p.id);
|
|
974
|
+
if (theirs.length < mine.length || mine.some((id, i) => theirs[i] !== id))
|
|
975
|
+
return undefined;
|
|
930
976
|
|
|
931
977
|
const steps = overlayNodes(existing.steps, incoming.steps);
|
|
932
978
|
if (!steps) return undefined;
|
|
933
979
|
|
|
934
|
-
|
|
980
|
+
const out: Flow = { ...existing, steps };
|
|
981
|
+
if (theirs.length > mine.length) {
|
|
982
|
+
out.participants = [
|
|
983
|
+
...existing.participants,
|
|
984
|
+
...incoming.participants.slice(mine.length),
|
|
985
|
+
];
|
|
986
|
+
}
|
|
987
|
+
const examples = [...(existing.examples ?? [])];
|
|
988
|
+
const held = new Set(examples.map((example) => example.id));
|
|
989
|
+
for (const example of incoming.examples ?? []) {
|
|
990
|
+
if (!held.has(example.id)) {
|
|
991
|
+
held.add(example.id);
|
|
992
|
+
examples.push(example);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (examples.length) out.examples = examples;
|
|
996
|
+
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* A node the second declaration added: a step seen in a recording and
|
|
1002
|
+
* unknown to the code, or a frame where the recordings parted - an `alt`
|
|
1003
|
+
* whose every step carries `seen` and whose every branch is counted. Its
|
|
1004
|
+
* id is not one the first declaration has, which is how it is told from a
|
|
1005
|
+
* declared node that moved.
|
|
1006
|
+
*/
|
|
1007
|
+
function isSeenAddition(node: FlowNode, against: FlowNode | undefined): boolean {
|
|
1008
|
+
if (against !== undefined && against.id === node.id) return false;
|
|
1009
|
+
if (node.type === "step") return node.seen !== undefined;
|
|
1010
|
+
if (node.type !== "alt") return false;
|
|
1011
|
+
let steps = 0;
|
|
1012
|
+
const allSeen = (nodes: FlowNode[]): boolean =>
|
|
1013
|
+
nodes.every((inner) => {
|
|
1014
|
+
if (inner.type === "step") {
|
|
1015
|
+
steps += 1;
|
|
1016
|
+
|
|
1017
|
+
return inner.seen !== undefined;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return inner.type === "alt" && inner.branches.every((b) => b.seen !== undefined && allSeen(b.steps));
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
return node.branches.every((b) => b.seen !== undefined && allSeen(b.steps)) && steps > 0;
|
|
935
1024
|
}
|
|
936
1025
|
|
|
937
1026
|
function overlayNodes(
|
|
938
1027
|
existing: FlowNode[],
|
|
939
1028
|
incoming: FlowNode[],
|
|
940
1029
|
): FlowNode[] | undefined {
|
|
941
|
-
if (existing.length !== incoming.length) return undefined;
|
|
942
1030
|
const out: FlowNode[] = [];
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
const
|
|
1031
|
+
let i = 0;
|
|
1032
|
+
for (const b of incoming) {
|
|
1033
|
+
const a = existing[i];
|
|
1034
|
+
if (isSeenAddition(b, a)) {
|
|
1035
|
+
out.push(b);
|
|
1036
|
+
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (a === undefined) return undefined;
|
|
1040
|
+
i++;
|
|
946
1041
|
if (a.type !== b.type || a.id !== b.id) return undefined;
|
|
947
1042
|
switch (a.type) {
|
|
948
1043
|
case "step": {
|
|
@@ -954,10 +1049,11 @@ function overlayNodes(
|
|
|
954
1049
|
(a.ref ?? "") !== (step.ref ?? "")
|
|
955
1050
|
)
|
|
956
1051
|
return undefined;
|
|
1052
|
+
const seen = step.seen ? { seen: step.seen } : {};
|
|
957
1053
|
if (step.status === "verified" && a.status === "declared") {
|
|
958
|
-
out.push({ ...a, status: "verified", note: a.note || step.note });
|
|
1054
|
+
out.push({ ...a, ...seen, status: "verified", note: a.note || step.note });
|
|
959
1055
|
} else {
|
|
960
|
-
out.push(a);
|
|
1056
|
+
out.push({ ...a, ...seen });
|
|
961
1057
|
}
|
|
962
1058
|
break;
|
|
963
1059
|
}
|
|
@@ -999,6 +1095,8 @@ function overlayNodes(
|
|
|
999
1095
|
}
|
|
1000
1096
|
}
|
|
1001
1097
|
|
|
1098
|
+
if (i !== existing.length) return undefined;
|
|
1099
|
+
|
|
1002
1100
|
return out;
|
|
1003
1101
|
}
|
|
1004
1102
|
|
|
@@ -1006,6 +1104,79 @@ function overlayNodes(
|
|
|
1006
1104
|
* Records who owns an id. Returns false when somebody already did, which is
|
|
1007
1105
|
* how a duplicate is skipped rather than appended twice.
|
|
1008
1106
|
*/
|
|
1107
|
+
/**
|
|
1108
|
+
* Lays one deployment over what is already held for its id.
|
|
1109
|
+
*
|
|
1110
|
+
* A manifest row and an api row fold into one: the api row's shape - it is
|
|
1111
|
+
* what runs, and carries the revision, the images and the link - with the
|
|
1112
|
+
* manifest filling what the deployer did not say (a service the labels
|
|
1113
|
+
* name, images the overlay pins when nothing runs yet), and the fields the
|
|
1114
|
+
* two disagree on kept as the tree's word under `drift`. Whichever arrives
|
|
1115
|
+
* first is held; the second folds onto it. Two rows of one basis are a
|
|
1116
|
+
* collision and the first is kept, said out loud.
|
|
1117
|
+
*/
|
|
1118
|
+
function foldDeployment(
|
|
1119
|
+
held: Deployment[],
|
|
1120
|
+
origins: Map<string, Map<DeploymentBasis, string>>,
|
|
1121
|
+
incoming: Deployment,
|
|
1122
|
+
path: string,
|
|
1123
|
+
conflicts: MergeConflict[],
|
|
1124
|
+
): void {
|
|
1125
|
+
const basis = deploymentBasis(incoming);
|
|
1126
|
+
const bases = origins.get(incoming.id) ?? new Map<DeploymentBasis, string>();
|
|
1127
|
+
origins.set(incoming.id, bases);
|
|
1128
|
+
const index = held.findIndex((d) => d.id === incoming.id);
|
|
1129
|
+
if (index < 0) {
|
|
1130
|
+
bases.set(basis, path);
|
|
1131
|
+
held.push(incoming);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const already = bases.get(basis);
|
|
1135
|
+
if (already !== undefined || basis === "both") {
|
|
1136
|
+
conflicts.push({
|
|
1137
|
+
path,
|
|
1138
|
+
where: incoming.id,
|
|
1139
|
+
message: `deployment "${incoming.id}" is ${basis === "manifest" ? "declared" : "listed"} here and in ${already ?? "another source"}; the first one is used`,
|
|
1140
|
+
});
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
bases.set(basis, path);
|
|
1144
|
+
const current = held[index]!;
|
|
1145
|
+
const [manifest, api] = basis === "manifest" ? [incoming, current] : [current, incoming];
|
|
1146
|
+
held[index] = laidOver(manifest, api);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** The api row with the manifest laid under it, and their differences as drift. */
|
|
1150
|
+
function laidOver(manifest: Deployment, api: Deployment): Deployment {
|
|
1151
|
+
const drift: DeploymentDrift = {};
|
|
1152
|
+
if (manifest.project && manifest.project !== api.project) drift.project = manifest.project;
|
|
1153
|
+
if (manifest.cluster && manifest.cluster !== api.cluster) drift.cluster = manifest.cluster;
|
|
1154
|
+
if (manifest.namespace && manifest.namespace !== api.namespace) drift.namespace = manifest.namespace;
|
|
1155
|
+
if (manifest.path && manifest.path !== api.path) drift.path = manifest.path;
|
|
1156
|
+
if (manifest.targetRevision && manifest.targetRevision !== api.targetRevision) {
|
|
1157
|
+
drift.targetRevision = manifest.targetRevision;
|
|
1158
|
+
}
|
|
1159
|
+
// The overlay pins some images; the cluster runs those and the base's.
|
|
1160
|
+
// Drift is a pinned image that is not running, not a running image the
|
|
1161
|
+
// overlay never mentioned.
|
|
1162
|
+
const running = new Set(api.images ?? []);
|
|
1163
|
+
const pinnedElsewhere = (manifest.images ?? []).filter((image) => !running.has(image));
|
|
1164
|
+
if (pinnedElsewhere.length > 0 && (api.images?.length ?? 0) > 0) drift.images = pinnedElsewhere;
|
|
1165
|
+
|
|
1166
|
+
const folded: Deployment = {
|
|
1167
|
+
...api,
|
|
1168
|
+
service: api.service ?? manifest.service,
|
|
1169
|
+
tool: api.tool || manifest.tool,
|
|
1170
|
+
images: api.images?.length ? api.images : manifest.images,
|
|
1171
|
+
basis: "both",
|
|
1172
|
+
};
|
|
1173
|
+
if (folded.service === undefined) delete folded.service;
|
|
1174
|
+
if (!folded.images?.length) delete folded.images;
|
|
1175
|
+
if (Object.keys(drift).length > 0) folded.drift = drift;
|
|
1176
|
+
else delete folded.drift;
|
|
1177
|
+
return folded;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1009
1180
|
function claim(
|
|
1010
1181
|
owners: Map<string, string>,
|
|
1011
1182
|
id: string,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { Link, useParams } from "react-router";
|
|
3
3
|
import { catalog, index } from "../data";
|
|
4
|
-
import { blockCounts, blockFields, enumsOf, rootEntity } from "../catalog";
|
|
4
|
+
import { allRepos, blockCounts, blockFields, enumsOf, rootEntity } from "../catalog";
|
|
5
5
|
import { isStatusEnum } from "../lib/shape";
|
|
6
6
|
import type {
|
|
7
7
|
Aggregate,
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
Service,
|
|
12
12
|
Enum,
|
|
13
13
|
} from "../catalog";
|
|
14
|
-
import { markdownOutline } from "../lib/derive";
|
|
14
|
+
import { flowsRunning, markdownOutline } from "../lib/derive";
|
|
15
15
|
import {
|
|
16
16
|
redisKeyspacesPersisting,
|
|
17
17
|
tablesPersisting,
|
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
} from "../routes";
|
|
34
34
|
import { methodId } from "../lib/api";
|
|
35
35
|
import { Markdown } from "../components/Markdown";
|
|
36
|
+
import { SourcePreviewLink } from "../components/SourcePreview";
|
|
37
|
+
import { sourceLocation, splitLine } from "../lib/source-link";
|
|
36
38
|
import { Empty, PageHeader, SectionTitle } from "../components/PageHeader";
|
|
37
39
|
import { Ident } from "../components/Ident";
|
|
38
40
|
import { RowActions } from "../components/RowActions";
|
|
@@ -77,14 +79,14 @@ function BuildingBlocks({
|
|
|
77
79
|
</div>
|
|
78
80
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 px-4 py-3">
|
|
79
81
|
<span className="mono flex items-center gap-1.5 text-muted">
|
|
80
|
-
root
|
|
82
|
+
{aggregate.kind === "model-group" ? "aggregate boundary" : "root"}
|
|
81
83
|
<KindIcon kind="entity" />
|
|
82
84
|
{rootTo ? (
|
|
83
85
|
<Link to={rootTo} className="text-ink hover:underline">
|
|
84
86
|
{aggregate.root}
|
|
85
87
|
</Link>
|
|
86
88
|
) : (
|
|
87
|
-
<span className="text-ink">{aggregate.root}</span>
|
|
89
|
+
<span className="text-ink">{aggregate.kind === "model-group" ? "not specified" : aggregate.root}</span>
|
|
88
90
|
)}
|
|
89
91
|
</span>
|
|
90
92
|
<span aria-hidden className="h-4 w-px bg-line-strong" />
|
|
@@ -265,10 +267,12 @@ function OperationList({
|
|
|
265
267
|
kind,
|
|
266
268
|
operations,
|
|
267
269
|
service,
|
|
270
|
+
aggregate,
|
|
268
271
|
}: {
|
|
269
272
|
kind: "command" | "query";
|
|
270
273
|
operations: Operation[];
|
|
271
274
|
service: Service;
|
|
275
|
+
aggregate: Aggregate;
|
|
272
276
|
}) {
|
|
273
277
|
// Whether this service records what exposes an operation at all. A catalog
|
|
274
278
|
// written before anything read a transport layer says nothing either way,
|
|
@@ -277,10 +281,14 @@ function OperationList({
|
|
|
277
281
|
aggregate.operations.some((operation) => operation.exposedBy?.length),
|
|
278
282
|
);
|
|
279
283
|
const to = servicePath(service.id);
|
|
284
|
+
const pins = allRepos(catalog);
|
|
280
285
|
|
|
281
286
|
return (
|
|
282
287
|
<ul className="flex flex-col gap-1">
|
|
283
|
-
{operations.map((op) =>
|
|
288
|
+
{operations.map((op) => {
|
|
289
|
+
const runs = flowsRunning(catalog, service, aggregate, op);
|
|
290
|
+
const location = op.source ? sourceLocation(op.source, service, pins) : null;
|
|
291
|
+
return (
|
|
284
292
|
<li
|
|
285
293
|
key={op.id}
|
|
286
294
|
className={`flex items-start gap-2 border-l-2 px-2 py-1.5 bg-surface ${
|
|
@@ -290,7 +298,7 @@ function OperationList({
|
|
|
290
298
|
<span className="mt-px shrink-0">
|
|
291
299
|
<KindIcon kind={kind} />
|
|
292
300
|
</span>
|
|
293
|
-
<div className="min-w-0">
|
|
301
|
+
<div className="min-w-0 flex-1">
|
|
294
302
|
<span className="flex flex-wrap items-center gap-x-2">
|
|
295
303
|
<Ident block value={op.id} className={op.deprecated ? "line-through" : undefined} />
|
|
296
304
|
{op.deprecated ? (
|
|
@@ -298,8 +306,49 @@ function OperationList({
|
|
|
298
306
|
deprecated
|
|
299
307
|
</span>
|
|
300
308
|
) : null}
|
|
309
|
+
{op.source ? (
|
|
310
|
+
<SourcePreviewLink location={location} className="mono ml-auto text-muted hover:text-ink">
|
|
311
|
+
{splitLine(op.source).path.split("/").pop()}
|
|
312
|
+
{splitLine(op.source).line ? `:${splitLine(op.source).line}` : ""}
|
|
313
|
+
</SourcePreviewLink>
|
|
314
|
+
) : null}
|
|
301
315
|
</span>
|
|
302
316
|
{op.doc ? <p className="mt-0.5 text-muted">{op.doc}</p> : null}
|
|
317
|
+
{/* What the caller hands in: the message's own shape. An empty
|
|
318
|
+
list is said out loud - a query that takes nothing is a fact
|
|
319
|
+
about the query, not a gap in the reading. */}
|
|
320
|
+
{op.fields ? (
|
|
321
|
+
op.fields.length ? (
|
|
322
|
+
<dl className="mono mt-1 grid grid-cols-[max-content_1fr] gap-x-3 gap-y-0.5 text-muted">
|
|
323
|
+
{op.fields.map((field) => (
|
|
324
|
+
<div key={field.name} className="contents">
|
|
325
|
+
<dt className={field.deprecated ? "line-through" : undefined}>{field.name}</dt>
|
|
326
|
+
<dd className="min-w-0 truncate">
|
|
327
|
+
<span className="text-ink">{field.type}</span>
|
|
328
|
+
{field.doc ? <span className="ml-2">{field.doc}</span> : null}
|
|
329
|
+
</dd>
|
|
330
|
+
</div>
|
|
331
|
+
))}
|
|
332
|
+
</dl>
|
|
333
|
+
) : (
|
|
334
|
+
<p className="mono mt-1 text-muted">takes nothing</p>
|
|
335
|
+
)
|
|
336
|
+
) : null}
|
|
337
|
+
{runs.length ? (
|
|
338
|
+
<p className="mono mt-1 flex flex-wrap items-center gap-x-2 text-muted">
|
|
339
|
+
<span>runs in</span>
|
|
340
|
+
{runs.map(({ flow, stepId }) => (
|
|
341
|
+
<Link
|
|
342
|
+
key={flow.slug}
|
|
343
|
+
to={paths.flowStep(flow.slug, stepId)}
|
|
344
|
+
className="rounded-control hover:text-ink"
|
|
345
|
+
title={flow.name}
|
|
346
|
+
>
|
|
347
|
+
{flow.slug}
|
|
348
|
+
</Link>
|
|
349
|
+
))}
|
|
350
|
+
</p>
|
|
351
|
+
) : null}
|
|
303
352
|
{op.exposedBy?.length ? (
|
|
304
353
|
<p className="mono mt-1 flex flex-wrap items-center gap-x-2 text-muted">
|
|
305
354
|
<span>exposed by</span>
|
|
@@ -328,7 +377,8 @@ function OperationList({
|
|
|
328
377
|
) : null}
|
|
329
378
|
</div>
|
|
330
379
|
</li>
|
|
331
|
-
|
|
380
|
+
);
|
|
381
|
+
})}
|
|
332
382
|
</ul>
|
|
333
383
|
);
|
|
334
384
|
}
|
|
@@ -385,7 +435,7 @@ export function AggregatePage() {
|
|
|
385
435
|
<PageHeader
|
|
386
436
|
kind={
|
|
387
437
|
<>
|
|
388
|
-
aggregate ·{" "}
|
|
438
|
+
{aggregate.kind === "model-group" ? "model group" : "aggregate"} ·{" "}
|
|
389
439
|
<Link
|
|
390
440
|
to={paths.service(context.id, service.slug)}
|
|
391
441
|
className="rounded-control hover:text-ink hover:underline"
|
|
@@ -410,6 +460,7 @@ export function AggregatePage() {
|
|
|
410
460
|
/>
|
|
411
461
|
|
|
412
462
|
<Markdown mermaid>{aggregate.readme}</Markdown>
|
|
463
|
+
{aggregate.kind === "model-group" ? <p className="mb-section text-muted">Models discovered in this application. Their relationships are extracted from the source; aggregate boundaries have not been specified.</p> : null}
|
|
413
464
|
|
|
414
465
|
<div
|
|
415
466
|
className="mt-section max-w-prose"
|
|
@@ -583,16 +634,16 @@ export function AggregatePage() {
|
|
|
583
634
|
anchor={AGGREGATE_ANCHOR.commands}
|
|
584
635
|
right={
|
|
585
636
|
<span>
|
|
586
|
-
they change the aggregate — one row lock each
|
|
637
|
+
{aggregate.kind === "model-group" ? "write operations discovered in this application" : "they change the aggregate — one row lock each"}
|
|
587
638
|
</span>
|
|
588
639
|
}
|
|
589
640
|
>
|
|
590
641
|
Commands
|
|
591
642
|
</SectionTitle>
|
|
592
643
|
{commands.length === 0 ? (
|
|
593
|
-
<Empty>nothing changes this aggregate from outside</Empty>
|
|
644
|
+
<Empty>{aggregate.kind === "model-group" ? "no application commands discovered" : "nothing changes this aggregate from outside"}</Empty>
|
|
594
645
|
) : null}
|
|
595
|
-
<OperationList kind="command" operations={commands} service={service} />
|
|
646
|
+
<OperationList kind="command" operations={commands} service={service} aggregate={aggregate} />
|
|
596
647
|
</div>
|
|
597
648
|
<div id={AGGREGATE_ANCHOR.queries}>
|
|
598
649
|
<SectionTitle
|
|
@@ -606,9 +657,9 @@ export function AggregatePage() {
|
|
|
606
657
|
Queries
|
|
607
658
|
</SectionTitle>
|
|
608
659
|
{queries.length === 0 ? (
|
|
609
|
-
<Empty>nothing reads this aggregate by name</Empty>
|
|
660
|
+
<Empty>{aggregate.kind === "model-group" ? "no application queries discovered" : "nothing reads this aggregate by name"}</Empty>
|
|
610
661
|
) : null}
|
|
611
|
-
<OperationList kind="query" operations={queries} service={service} />
|
|
662
|
+
<OperationList kind="query" operations={queries} service={service} aggregate={aggregate} />
|
|
612
663
|
</div>
|
|
613
664
|
</div>
|
|
614
665
|
|
|
@@ -750,7 +801,7 @@ export function AggregatePage() {
|
|
|
750
801
|
/>
|
|
751
802
|
</div>
|
|
752
803
|
|
|
753
|
-
<Toc items={toc} label="Sections of this aggregate" title="Outline" />
|
|
804
|
+
<Toc items={toc} label={aggregate.kind === "model-group" ? "Sections of this model group" : "Sections of this aggregate"} title="Outline" />
|
|
754
805
|
</div>
|
|
755
806
|
</div>
|
|
756
807
|
);
|
package/src/pages/ContextMap.tsx
CHANGED
|
@@ -17,6 +17,8 @@ import { Link, useSearchParams } from "react-router";
|
|
|
17
17
|
import { ArrowRight, ArrowLeftRight, Boxes, Minus } from "lucide-react";
|
|
18
18
|
import { catalog } from "../data";
|
|
19
19
|
import { plural } from "../lib/format";
|
|
20
|
+
import { narrowToEnvironments } from "../lib/environments";
|
|
21
|
+
import { environmentsOf } from "../likec4/ids";
|
|
20
22
|
import { PATTERN_LABEL, PATTERN_MEANING, contextMap } from "../lib/context-map";
|
|
21
23
|
import type {
|
|
22
24
|
ContextDependency,
|
|
@@ -439,7 +441,22 @@ export function ContextMap() {
|
|
|
439
441
|
useDocumentTitle("Context map");
|
|
440
442
|
const [search, setSearch] = useSearchParams();
|
|
441
443
|
const [tour, setTour] = useState(() => search.get("tour") === "1");
|
|
442
|
-
|
|
444
|
+
// The map of one environment is the same map over the services deployed
|
|
445
|
+
// there: a pair joined only through a service that is not in staging is
|
|
446
|
+
// not joined in staging. Empty means the whole estate, as every filter here.
|
|
447
|
+
const environmentChips = useMemo(() => environmentsOf(catalog), []);
|
|
448
|
+
const [environments, setEnvironments] = useState<Set<string>>(new Set());
|
|
449
|
+
const narrowed = useMemo(
|
|
450
|
+
() => narrowToEnvironments(catalog, environments),
|
|
451
|
+
[environments],
|
|
452
|
+
);
|
|
453
|
+
const relations = useMemo(() => contextMap(narrowed), [narrowed]);
|
|
454
|
+
const toggleEnvironment = (env: string) =>
|
|
455
|
+
setEnvironments((prev) => {
|
|
456
|
+
const next = new Set(prev);
|
|
457
|
+
if (!next.delete(env)) next.add(env);
|
|
458
|
+
return next;
|
|
459
|
+
});
|
|
443
460
|
const joined = relations.filter(
|
|
444
461
|
(r) => r.dependencies.length > 0 || r.shared.length > 0,
|
|
445
462
|
);
|
|
@@ -457,9 +474,34 @@ export function ContextMap() {
|
|
|
457
474
|
<div className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-2 border-b px-gutter py-3 border-line">
|
|
458
475
|
<h1 className="text-lg font-semibold">Context map</h1>
|
|
459
476
|
<span className="mono text-muted">
|
|
460
|
-
{
|
|
477
|
+
{narrowed.contexts.length} {plural(narrowed.contexts.length, "domain")} ·{" "}
|
|
461
478
|
{wired} of {relations.length} {plural(relations.length, "pair")} joined
|
|
462
479
|
</span>
|
|
480
|
+
{environmentChips.length > 0 ? (
|
|
481
|
+
<div className="seg" role="group" aria-label="Filter by environment">
|
|
482
|
+
{environmentChips.map((env) => {
|
|
483
|
+
const on = environments.has(env);
|
|
484
|
+
return (
|
|
485
|
+
<button
|
|
486
|
+
key={env}
|
|
487
|
+
type="button"
|
|
488
|
+
onClick={() => toggleEnvironment(env)}
|
|
489
|
+
aria-pressed={on}
|
|
490
|
+
title={`only the services the deployer places in ${env}`}
|
|
491
|
+
className="flex items-center gap-1.5"
|
|
492
|
+
style={{
|
|
493
|
+
color: on ? "var(--accent)" : "var(--fg-muted)",
|
|
494
|
+
background: on
|
|
495
|
+
? "color-mix(in srgb, var(--accent) 12%, transparent)"
|
|
496
|
+
: undefined,
|
|
497
|
+
}}
|
|
498
|
+
>
|
|
499
|
+
{env}
|
|
500
|
+
</button>
|
|
501
|
+
);
|
|
502
|
+
})}
|
|
503
|
+
</div>
|
|
504
|
+
) : null}
|
|
463
505
|
|
|
464
506
|
{/* Two legends, and they answer different questions: what the arrow
|
|
465
507
|
means, and how far a word on a chip can be trusted. */}
|
|
@@ -505,7 +547,7 @@ export function ContextMap() {
|
|
|
505
547
|
</SectionTitle>
|
|
506
548
|
<div className="h-[340px] overflow-hidden rounded-card border border-line">
|
|
507
549
|
<ContextMapPane
|
|
508
|
-
catalog={
|
|
550
|
+
catalog={narrowed}
|
|
509
551
|
relations={relations}
|
|
510
552
|
zoomOnScroll={false}
|
|
511
553
|
/>
|