@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
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { Catalog, Service } from "../catalog";
|
|
3
|
+
import { catalog } from "../testing/estate";
|
|
4
|
+
import { BROKER_SHARE, bridges, centrality, serviceGraph } from "./centrality";
|
|
5
|
+
|
|
6
|
+
// A catalog reduced to what the graph reads: contexts, services and the
|
|
7
|
+
// calls between them. `a -> b` is a call from a to b; the graph does not
|
|
8
|
+
// care which way, only that the two are joined.
|
|
9
|
+
function estate(
|
|
10
|
+
contexts: Record<string, string[]>,
|
|
11
|
+
edges: string[],
|
|
12
|
+
consumers: Record<string, string[]> = {},
|
|
13
|
+
): Catalog {
|
|
14
|
+
const service = (context: string, slug: string): Service => {
|
|
15
|
+
const id = `${context}.${slug}`;
|
|
16
|
+
return {
|
|
17
|
+
id,
|
|
18
|
+
slug,
|
|
19
|
+
name: slug,
|
|
20
|
+
repo: "",
|
|
21
|
+
path: "",
|
|
22
|
+
readme: "",
|
|
23
|
+
provides: [],
|
|
24
|
+
consumes: edges
|
|
25
|
+
.filter((e) => e.startsWith(`${id} -> `))
|
|
26
|
+
.map((e) => ({
|
|
27
|
+
id: `rpc/${e}`,
|
|
28
|
+
peer: e.slice(id.length + 4),
|
|
29
|
+
status: "verified" as const,
|
|
30
|
+
source: "test",
|
|
31
|
+
})),
|
|
32
|
+
aggregates:
|
|
33
|
+
consumers[id] === undefined
|
|
34
|
+
? []
|
|
35
|
+
: [
|
|
36
|
+
{
|
|
37
|
+
id: `${id}.root`,
|
|
38
|
+
slug: "root",
|
|
39
|
+
name: "root",
|
|
40
|
+
readme: "",
|
|
41
|
+
root: "root",
|
|
42
|
+
entities: [],
|
|
43
|
+
valueObjects: [],
|
|
44
|
+
operations: [],
|
|
45
|
+
events: [
|
|
46
|
+
{
|
|
47
|
+
id: `${id}.root.Happened`,
|
|
48
|
+
slug: "happened",
|
|
49
|
+
name: "Happened",
|
|
50
|
+
versions: [],
|
|
51
|
+
consumers: (consumers[id] ?? []).map((s) => ({
|
|
52
|
+
service: s,
|
|
53
|
+
status: "verified" as const,
|
|
54
|
+
})),
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
generatedAt: "2026-01-01T00:00:00Z",
|
|
63
|
+
commit: "0000000",
|
|
64
|
+
contexts: Object.entries(contexts).map(([id, slugs]) => ({
|
|
65
|
+
id,
|
|
66
|
+
slug: id,
|
|
67
|
+
name: id,
|
|
68
|
+
summary: "",
|
|
69
|
+
services: slugs.map((slug) => service(id, slug)),
|
|
70
|
+
})),
|
|
71
|
+
defs: {},
|
|
72
|
+
flows: [],
|
|
73
|
+
adrs: [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const by = <T extends { id: string }>(list: T[], id: string): T =>
|
|
78
|
+
list.find((c) => c.id === id) ?? expect.fail(`no ${id}`);
|
|
79
|
+
|
|
80
|
+
describe("serviceGraph", () => {
|
|
81
|
+
it("joins a pair once whatever joins them, and both ways", () => {
|
|
82
|
+
const graph = serviceGraph(
|
|
83
|
+
estate({ a: ["x", "y"] }, ["a.x -> a.y", "a.y -> a.x"], {
|
|
84
|
+
"a.x": ["a.y"],
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
expect(graph.adjacent.get("a.x")).toEqual(["a.y"]);
|
|
88
|
+
expect(graph.adjacent.get("a.y")).toEqual(["a.x"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("leaves out ghosts, unresolved peers and a service consuming itself", () => {
|
|
92
|
+
const graph = serviceGraph(
|
|
93
|
+
estate({ a: ["x"] }, ["a.x -> fraud-scoring", "a.x -> a.x"], {
|
|
94
|
+
"a.x": ["analytics-sink", "a.x"],
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
expect(graph.adjacent.get("a.x")).toEqual([]);
|
|
98
|
+
expect(graph.nodes).toEqual(["a.x"]);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("centrality", () => {
|
|
103
|
+
it("is zero everywhere on an estate too small to have a middle", () => {
|
|
104
|
+
const two = centrality(estate({ a: ["x", "y"] }, ["a.x -> a.y"]));
|
|
105
|
+
expect(two.map((c) => c.betweenness)).toEqual([0, 0]);
|
|
106
|
+
expect(two.map((c) => c.degree)).toEqual([1, 1]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("puts the whole of a path through its middle and none through its ends", () => {
|
|
110
|
+
// x - y - z: the only pair not touching y is none, so y carries 1 of 1.
|
|
111
|
+
const path = centrality(
|
|
112
|
+
estate({ a: ["x", "y", "z"] }, ["a.x -> a.y", "a.y -> a.z"]),
|
|
113
|
+
);
|
|
114
|
+
expect(by(path, "a.y").betweenness).toBe(1);
|
|
115
|
+
expect(by(path, "a.x").betweenness).toBe(0);
|
|
116
|
+
expect(by(path, "a.z").betweenness).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("gives a star's hub everything and its leaves nothing", () => {
|
|
120
|
+
const star = centrality(
|
|
121
|
+
estate({ a: ["hub", "p", "q", "r"] }, [
|
|
122
|
+
"a.p -> a.hub",
|
|
123
|
+
"a.q -> a.hub",
|
|
124
|
+
"a.r -> a.hub",
|
|
125
|
+
]),
|
|
126
|
+
);
|
|
127
|
+
expect(by(star, "a.hub").betweenness).toBe(1);
|
|
128
|
+
expect(by(star, "a.hub").degree).toBe(3);
|
|
129
|
+
expect(by(star, "a.p").betweenness).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("splits a pair's paths between two equal roads", () => {
|
|
133
|
+
// x - y - z and x - w - z: y and w each carry half of the one pair.
|
|
134
|
+
const square = centrality(
|
|
135
|
+
estate({ a: ["x", "y", "w", "z"] }, [
|
|
136
|
+
"a.x -> a.y",
|
|
137
|
+
"a.x -> a.w",
|
|
138
|
+
"a.y -> a.z",
|
|
139
|
+
"a.w -> a.z",
|
|
140
|
+
]),
|
|
141
|
+
);
|
|
142
|
+
// Three pairs not touching y: (x,z) half through y, (x,w) and (w,z) not.
|
|
143
|
+
expect(by(square, "a.y").betweenness).toBeCloseTo(0.5 / 3);
|
|
144
|
+
expect(by(square, "a.w").betweenness).toBeCloseTo(0.5 / 3);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("names the context pairs a service is the road between", () => {
|
|
148
|
+
// auth.a - shop.gw - shop.core - pay.p: the gateway is the only road
|
|
149
|
+
// from auth to shop and to pay; core is the only road to pay from both.
|
|
150
|
+
const c = centrality(
|
|
151
|
+
estate({ auth: ["a"], shop: ["gw", "core"], pay: ["p"] }, [
|
|
152
|
+
"auth.a -> shop.gw",
|
|
153
|
+
"shop.gw -> shop.core",
|
|
154
|
+
"shop.core -> pay.p",
|
|
155
|
+
]),
|
|
156
|
+
);
|
|
157
|
+
expect(by(c, "shop.gw").between).toEqual([
|
|
158
|
+
{ a: "auth", b: "shop", share: 1 },
|
|
159
|
+
{ a: "auth", b: "pay", share: 1 },
|
|
160
|
+
]);
|
|
161
|
+
expect(by(c, "shop.core").between).toEqual([
|
|
162
|
+
{ a: "auth", b: "pay", share: 1 },
|
|
163
|
+
{ a: "shop", b: "pay", share: 1 },
|
|
164
|
+
]);
|
|
165
|
+
expect(by(c, "auth.a").between).toEqual([]);
|
|
166
|
+
expect(by(c, "pay.p").between).toEqual([]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("does not call a service the road between two contexts when there is another as good", () => {
|
|
170
|
+
// auth.a reaches pay.p through shop.x or shop.y: each carries exactly
|
|
171
|
+
// half, and half is not more than half, so neither stands between.
|
|
172
|
+
const even = centrality(
|
|
173
|
+
estate({ auth: ["a"], shop: ["x", "y"], pay: ["p"] }, [
|
|
174
|
+
"auth.a -> shop.x",
|
|
175
|
+
"auth.a -> shop.y",
|
|
176
|
+
"shop.x -> pay.p",
|
|
177
|
+
"shop.y -> pay.p",
|
|
178
|
+
]),
|
|
179
|
+
);
|
|
180
|
+
expect(by(even, "shop.x").between).toEqual([]);
|
|
181
|
+
expect(by(even, "shop.y").between).toEqual([]);
|
|
182
|
+
expect(by(even, "shop.x").betweenness).toBeGreaterThan(0);
|
|
183
|
+
// A second auth service that reaches pay only through x tilts it: x now
|
|
184
|
+
// carries (1 + 0.5) of the 2 pairs, y carries 0.5 of them.
|
|
185
|
+
const tilted = centrality(
|
|
186
|
+
estate({ auth: ["a1", "a2"], shop: ["x", "y"], pay: ["p"] }, [
|
|
187
|
+
"auth.a1 -> shop.x",
|
|
188
|
+
"auth.a1 -> shop.y",
|
|
189
|
+
"auth.a2 -> shop.x",
|
|
190
|
+
"shop.x -> pay.p",
|
|
191
|
+
"shop.y -> pay.p",
|
|
192
|
+
]),
|
|
193
|
+
);
|
|
194
|
+
expect(by(tilted, "shop.x").between).toEqual([
|
|
195
|
+
{ a: "auth", b: "pay", share: 0.75 },
|
|
196
|
+
]);
|
|
197
|
+
expect(by(tilted, "shop.y").between).toEqual([]);
|
|
198
|
+
expect(BROKER_SHARE).toBe(0.5);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("does not count the roads a service is at the end of as roads it is between", () => {
|
|
202
|
+
// auth.a - shop.gw - shop.core: (a, gw) is a road gw is ON. Between
|
|
203
|
+
// auth and shop, gw carries the one remaining pair (a, core) in full.
|
|
204
|
+
const c = centrality(
|
|
205
|
+
estate({ auth: ["a"], shop: ["gw", "core"] }, [
|
|
206
|
+
"auth.a -> shop.gw",
|
|
207
|
+
"shop.gw -> shop.core",
|
|
208
|
+
]),
|
|
209
|
+
);
|
|
210
|
+
expect(by(c, "shop.gw").between).toEqual([{ a: "auth", b: "shop", share: 1 }]);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("bridges", () => {
|
|
215
|
+
it("lists the estate's roads between contexts, widest first", () => {
|
|
216
|
+
// In the frozen estate auth reaches everything through cart, and cart
|
|
217
|
+
// reaches everything but auth through pricing; oms is the only way from
|
|
218
|
+
// shop and auth into payments and delivery.
|
|
219
|
+
const list = bridges(catalog);
|
|
220
|
+
expect(list.map((b) => b.id)).toEqual([
|
|
221
|
+
"shop.oms",
|
|
222
|
+
"shop.pricing",
|
|
223
|
+
"shop.cart",
|
|
224
|
+
]);
|
|
225
|
+
expect(by(list, "shop.cart").between).toEqual([
|
|
226
|
+
{ a: "shop", b: "auth", share: 1 },
|
|
227
|
+
{ a: "payments", b: "auth", share: 1 },
|
|
228
|
+
{ a: "delivery", b: "auth", share: 1 },
|
|
229
|
+
]);
|
|
230
|
+
expect(by(list, "shop.oms").between.map((p) => `${p.a}~${p.b}`)).toEqual([
|
|
231
|
+
"shop~payments",
|
|
232
|
+
"shop~delivery",
|
|
233
|
+
"payments~auth",
|
|
234
|
+
"delivery~auth",
|
|
235
|
+
]);
|
|
236
|
+
for (const b of list) {
|
|
237
|
+
expect(b.betweenness).toBeGreaterThan(0);
|
|
238
|
+
expect(b.betweenness).toBeLessThanOrEqual(1);
|
|
239
|
+
}
|
|
240
|
+
expect(list.map((b) => b.betweenness)).toEqual(
|
|
241
|
+
[...list.map((b) => b.betweenness)].sort((x, y) => y - x),
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("leaves a leaf out however many neighbours it has", () => {
|
|
246
|
+
const star = bridges(
|
|
247
|
+
estate({ a: ["hub"], b: ["p"], c: ["q"] }, ["b.p -> a.hub", "c.q -> a.hub"]),
|
|
248
|
+
);
|
|
249
|
+
expect(star.map((b) => b.id)).toEqual(["a.hub"]);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Which services the estate's paths run through.
|
|
2
|
+
//
|
|
3
|
+
// The dependency graph shows who talks to whom; the context map shows which
|
|
4
|
+
// contexts depend on which. Neither answers a third question a reader asks
|
|
5
|
+
// before touching a service: if this one goes, what stops reaching what? A
|
|
6
|
+
// service with three neighbours can be the only road between two contexts,
|
|
7
|
+
// and a service with twelve can be a leaf of every one of them. Degree does
|
|
8
|
+
// not tell them apart. Betweenness does: the share of shortest paths between
|
|
9
|
+
// every pair of services that pass through this one.
|
|
10
|
+
//
|
|
11
|
+
// The graph is the same two facts the context map is drawn from - an event
|
|
12
|
+
// with a consumer, a call with a peer - collapsed to one undirected edge per
|
|
13
|
+
// pair of services. Undirected, because the question is "who is between",
|
|
14
|
+
// and a reader who asks it means the road, not which way the traffic runs.
|
|
15
|
+
// Unweighted, because a pair joined by one call and a pair joined by twenty
|
|
16
|
+
// are both joined.
|
|
17
|
+
//
|
|
18
|
+
// Two things are counted, and the page says which is which:
|
|
19
|
+
//
|
|
20
|
+
// betweenness - Brandes' number, normalised so 1 means every shortest path
|
|
21
|
+
// between every other pair runs through this service. It ranks.
|
|
22
|
+
// between - the pairs of CONTEXTS for which more than half of the
|
|
23
|
+
// shortest paths between their services run through this one.
|
|
24
|
+
// It is the finding: "auth reaches payments only through cart".
|
|
25
|
+
//
|
|
26
|
+
// Consumers with no service of their own and calls to a peer the catalog does
|
|
27
|
+
// not know are left out. A ghost has no context to be between, and an
|
|
28
|
+
// unresolved peer is a problem the problems page already lists.
|
|
29
|
+
|
|
30
|
+
import type { Catalog } from "../catalog";
|
|
31
|
+
|
|
32
|
+
/** A pair of contexts this service stands between. */
|
|
33
|
+
export interface Brokered {
|
|
34
|
+
/** The two contexts, in catalog order. */
|
|
35
|
+
a: string;
|
|
36
|
+
b: string;
|
|
37
|
+
/** Share of shortest paths between the two that pass through the service. */
|
|
38
|
+
share: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Centrality {
|
|
42
|
+
id: string;
|
|
43
|
+
context: string;
|
|
44
|
+
/** Distinct services it exchanges anything with. */
|
|
45
|
+
degree: number;
|
|
46
|
+
/** Normalised to 0..1; 0 on an estate of fewer than three services. */
|
|
47
|
+
betweenness: number;
|
|
48
|
+
between: Brokered[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The share of a context pair's paths a service must carry MORE than to be
|
|
53
|
+
* said to stand between them. Half, strictly: two equal roads are two roads,
|
|
54
|
+
* and a service on one of them is not what the other side depends on.
|
|
55
|
+
*/
|
|
56
|
+
export const BROKER_SHARE = 0.5;
|
|
57
|
+
|
|
58
|
+
export interface ServiceGraph {
|
|
59
|
+
/** Service ids in catalog order. */
|
|
60
|
+
nodes: string[];
|
|
61
|
+
/** Service id -> context id. */
|
|
62
|
+
context: Map<string, string>;
|
|
63
|
+
/** Undirected adjacency, neighbours in first-seen order. */
|
|
64
|
+
adjacent: Map<string, string[]>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One undirected edge per pair of catalog services that exchange anything. */
|
|
68
|
+
export function serviceGraph(catalog: Catalog): ServiceGraph {
|
|
69
|
+
const nodes: string[] = [];
|
|
70
|
+
const context = new Map<string, string>();
|
|
71
|
+
for (const ctx of catalog.contexts) {
|
|
72
|
+
for (const service of ctx.services) {
|
|
73
|
+
nodes.push(service.id);
|
|
74
|
+
context.set(service.id, ctx.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const adjacent = new Map<string, string[]>(nodes.map((id) => [id, []]));
|
|
79
|
+
const join = (a: string, b: string): void => {
|
|
80
|
+
if (a === b || !context.has(a) || !context.has(b)) return;
|
|
81
|
+
const ofA = adjacent.get(a) as string[];
|
|
82
|
+
if (ofA.includes(b)) return;
|
|
83
|
+
ofA.push(b);
|
|
84
|
+
(adjacent.get(b) as string[]).push(a);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
for (const ctx of catalog.contexts) {
|
|
88
|
+
for (const service of ctx.services) {
|
|
89
|
+
for (const aggregate of service.aggregates) {
|
|
90
|
+
for (const event of aggregate.events) {
|
|
91
|
+
for (const consumer of event.consumers) join(service.id, consumer.service);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const call of service.consumes) join(service.id, call.peer);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { nodes, context, adjacent };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Brandes' algorithm, with the dependency of each source split by the target's
|
|
102
|
+
* context, so the same pass that sums betweenness also says which context
|
|
103
|
+
* pairs a service carries.
|
|
104
|
+
*/
|
|
105
|
+
export function centrality(catalog: Catalog): Centrality[] {
|
|
106
|
+
const graph = serviceGraph(catalog);
|
|
107
|
+
const { nodes, context, adjacent } = graph;
|
|
108
|
+
const n = nodes.length;
|
|
109
|
+
const order = catalog.contexts.map((c) => c.id);
|
|
110
|
+
const rank = new Map(order.map((id, i) => [id, i]));
|
|
111
|
+
|
|
112
|
+
const raw = new Map<string, number>(nodes.map((id) => [id, 0]));
|
|
113
|
+
// node -> (source context ~ target context) -> summed dependency.
|
|
114
|
+
const carried = new Map<string, Map<string, number>>(
|
|
115
|
+
nodes.map((id) => [id, new Map()]),
|
|
116
|
+
);
|
|
117
|
+
// source -> target context -> reachable targets (the source excluded).
|
|
118
|
+
const reach = new Map<string, Map<string, number>>();
|
|
119
|
+
|
|
120
|
+
for (const s of nodes) {
|
|
121
|
+
const stack: string[] = [];
|
|
122
|
+
const pred = new Map<string, string[]>(nodes.map((id) => [id, []]));
|
|
123
|
+
const sigma = new Map<string, number>(nodes.map((id) => [id, 0]));
|
|
124
|
+
const dist = new Map<string, number>(nodes.map((id) => [id, -1]));
|
|
125
|
+
sigma.set(s, 1);
|
|
126
|
+
dist.set(s, 0);
|
|
127
|
+
const queue: string[] = [s];
|
|
128
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
129
|
+
const v = queue[head] as string;
|
|
130
|
+
stack.push(v);
|
|
131
|
+
const dv = dist.get(v) as number;
|
|
132
|
+
for (const w of adjacent.get(v) as string[]) {
|
|
133
|
+
if ((dist.get(w) as number) < 0) {
|
|
134
|
+
dist.set(w, dv + 1);
|
|
135
|
+
queue.push(w);
|
|
136
|
+
}
|
|
137
|
+
if ((dist.get(w) as number) === dv + 1) {
|
|
138
|
+
sigma.set(w, (sigma.get(w) as number) + (sigma.get(v) as number));
|
|
139
|
+
(pred.get(w) as string[]).push(v);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const seen = new Map<string, number>();
|
|
145
|
+
for (const t of stack) {
|
|
146
|
+
if (t === s) continue;
|
|
147
|
+
const c = context.get(t) as string;
|
|
148
|
+
seen.set(c, (seen.get(c) ?? 0) + 1);
|
|
149
|
+
}
|
|
150
|
+
reach.set(s, seen);
|
|
151
|
+
|
|
152
|
+
// delta[w][targetContext]: dependency of s on w, over targets in that context.
|
|
153
|
+
const delta = new Map<string, Map<string, number>>(
|
|
154
|
+
nodes.map((id) => [id, new Map()]),
|
|
155
|
+
);
|
|
156
|
+
while (stack.length > 0) {
|
|
157
|
+
const w = stack.pop() as string;
|
|
158
|
+
const dw = delta.get(w) as Map<string, number>;
|
|
159
|
+
const own = context.get(w) as string;
|
|
160
|
+
for (const v of pred.get(w) as string[]) {
|
|
161
|
+
const ratio = (sigma.get(v) as number) / (sigma.get(w) as number);
|
|
162
|
+
const dv = delta.get(v) as Map<string, number>;
|
|
163
|
+
dv.set(own, (dv.get(own) ?? 0) + ratio);
|
|
164
|
+
for (const [c, d] of dw) dv.set(c, (dv.get(c) ?? 0) + ratio * d);
|
|
165
|
+
}
|
|
166
|
+
if (w === s) continue;
|
|
167
|
+
const from = context.get(s) as string;
|
|
168
|
+
const bucket = carried.get(w) as Map<string, number>;
|
|
169
|
+
let sum = 0;
|
|
170
|
+
for (const [to, d] of dw) {
|
|
171
|
+
sum += d;
|
|
172
|
+
const key = pairKey(from, to, rank);
|
|
173
|
+
bucket.set(key, (bucket.get(key) ?? 0) + d);
|
|
174
|
+
}
|
|
175
|
+
raw.set(w, (raw.get(w) as number) + sum);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Ordered (u, v) pairs with u in A and v in B, both reachable, any u and v.
|
|
180
|
+
const pairs = (a: string, b: string): number => {
|
|
181
|
+
let count = 0;
|
|
182
|
+
for (const s of nodes) {
|
|
183
|
+
if (context.get(s) !== a) continue;
|
|
184
|
+
count += reach.get(s)?.get(b) ?? 0;
|
|
185
|
+
}
|
|
186
|
+
return count;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const scale = n < 3 ? 0 : 2 / ((n - 1) * (n - 2));
|
|
190
|
+
return nodes.map((id) => {
|
|
191
|
+
const own = context.get(id) as string;
|
|
192
|
+
const between: Brokered[] = [];
|
|
193
|
+
for (const [key, sum] of carried.get(id) as Map<string, number>) {
|
|
194
|
+
const [a, b] = key.split("~") as [string, string];
|
|
195
|
+
// Every (u, v) was counted from both ends; and pairs with the service
|
|
196
|
+
// itself at an end are roads it is on, not roads it is between.
|
|
197
|
+
let total = a === b ? pairs(a, a) / 2 : pairs(a, b);
|
|
198
|
+
if (own === a) total -= reach.get(id)?.get(b) ?? 0;
|
|
199
|
+
else if (own === b) total -= reach.get(id)?.get(a) ?? 0;
|
|
200
|
+
const share = total > 0 ? sum / 2 / total : 0;
|
|
201
|
+
if (a !== b && share > BROKER_SHARE) between.push({ a, b, share });
|
|
202
|
+
}
|
|
203
|
+
between.sort(
|
|
204
|
+
(x, y) =>
|
|
205
|
+
y.share - x.share ||
|
|
206
|
+
(rank.get(x.a) ?? 0) - (rank.get(y.a) ?? 0) ||
|
|
207
|
+
(rank.get(x.b) ?? 0) - (rank.get(y.b) ?? 0),
|
|
208
|
+
);
|
|
209
|
+
return {
|
|
210
|
+
id,
|
|
211
|
+
context: own,
|
|
212
|
+
degree: (adjacent.get(id) as string[]).length,
|
|
213
|
+
betweenness: ((raw.get(id) as number) / 2) * scale,
|
|
214
|
+
between,
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function pairKey(a: string, b: string, rank: Map<string, number>): string {
|
|
220
|
+
return (rank.get(a) ?? 0) <= (rank.get(b) ?? 0) ? `${a}~${b}` : `${b}~${a}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The services worth a reader's attention: on some shortest path, and the
|
|
225
|
+
* main road between at least one pair of contexts. Highest betweenness first,
|
|
226
|
+
* catalog order between equals.
|
|
227
|
+
*/
|
|
228
|
+
export function bridges(catalog: Catalog): Centrality[] {
|
|
229
|
+
return centrality(catalog)
|
|
230
|
+
.filter((c) => c.betweenness > 0 && c.between.length > 0)
|
|
231
|
+
.sort((x, y) => y.betweenness - x.betweenness);
|
|
232
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { confluenceSearchUrl } from "./confluence";
|
|
3
|
+
|
|
4
|
+
describe("Confluence integration", () => {
|
|
5
|
+
it("searches the whole Cloud site from a site URL", () => {
|
|
6
|
+
expect(confluenceSearchUrl("https://acme.atlassian.net/wiki", "shop.oms")).toBe(
|
|
7
|
+
"https://acme.atlassian.net/wiki/search?text=shop.oms",
|
|
8
|
+
);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("narrows the search to the space a Cloud URL names", () => {
|
|
12
|
+
expect(
|
|
13
|
+
confluenceSearchUrl(
|
|
14
|
+
"https://acme.atlassian.net/wiki/spaces/ARCH/overview",
|
|
15
|
+
"Order management",
|
|
16
|
+
),
|
|
17
|
+
).toBe(
|
|
18
|
+
"https://acme.atlassian.net/wiki/search?text=Order+management&spaces=ARCH",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("uses the Server search page behind a context path", () => {
|
|
23
|
+
expect(
|
|
24
|
+
confluenceSearchUrl("https://wiki.example/confluence/display/ARCH", "shop.oms"),
|
|
25
|
+
).toBe(
|
|
26
|
+
"https://wiki.example/confluence/dosearchsite.action?queryString=shop.oms&where=ARCH",
|
|
27
|
+
);
|
|
28
|
+
expect(confluenceSearchUrl("https://wiki.example/", "shop.oms")).toBe(
|
|
29
|
+
"https://wiki.example/dosearchsite.action?queryString=shop.oms",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("offers nothing before a URL is configured or when it is unsafe", () => {
|
|
34
|
+
expect(confluenceSearchUrl("", "shop.oms")).toBeNull();
|
|
35
|
+
expect(confluenceSearchUrl("javascript:alert(1)", "shop.oms")).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The optional hand-off from a catalogued service or context to the wiki
|
|
2
|
+
// where its team writes.
|
|
3
|
+
//
|
|
4
|
+
// The catalog carries no page ids, so the link is a search by name: the one
|
|
5
|
+
// address Confluence answers for any installation. A site URL searches the
|
|
6
|
+
// whole site; a URL that names a space narrows the search to it. Cloud and
|
|
7
|
+
// Server answer at different paths, told apart by the `/wiki` prefix Cloud
|
|
8
|
+
// always has.
|
|
9
|
+
|
|
10
|
+
import { createIntegrationStore, normalizeIntegrationUrl } from "./integration-url";
|
|
11
|
+
|
|
12
|
+
export const CONFLUENCE_KEY = "portolan.integrations.confluence";
|
|
13
|
+
|
|
14
|
+
/** The search page for `name`, on the site or in the space the URL names. */
|
|
15
|
+
export function confluenceSearchUrl(
|
|
16
|
+
configured: string,
|
|
17
|
+
name: string,
|
|
18
|
+
): string | null {
|
|
19
|
+
const normalized = normalizeIntegrationUrl(configured);
|
|
20
|
+
if (!normalized) return null;
|
|
21
|
+
const url = new URL(normalized);
|
|
22
|
+
const path = url.pathname;
|
|
23
|
+
const space = path.match(/\/(?:spaces|display)\/([^/]+)/)?.[1];
|
|
24
|
+
const cloud = path.match(/^(.*\/wiki)(?:\/.*)?$/)?.[1];
|
|
25
|
+
const params = new URLSearchParams();
|
|
26
|
+
if (cloud !== undefined) {
|
|
27
|
+
url.pathname = `${cloud}/search`;
|
|
28
|
+
params.set("text", name);
|
|
29
|
+
if (space) params.set("spaces", space);
|
|
30
|
+
} else {
|
|
31
|
+
const base = path.replace(/\/(?:spaces|display)\/.*$/, "").replace(/\/$/, "");
|
|
32
|
+
url.pathname = `${base}/dosearchsite.action`;
|
|
33
|
+
params.set("queryString", name);
|
|
34
|
+
if (space) params.set("where", space);
|
|
35
|
+
}
|
|
36
|
+
url.search = params.toString();
|
|
37
|
+
url.hash = "";
|
|
38
|
+
return url.toString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const useConfluence = createIntegrationStore(CONFLUENCE_KEY);
|
package/src/lib/context-color.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CSSProperties } from "react";
|
|
2
2
|
import { catalog } from "../data";
|
|
3
3
|
|
|
4
|
-
const CTX_SLOTS = 6;
|
|
4
|
+
export const CTX_SLOTS = 6;
|
|
5
5
|
|
|
6
6
|
/** Context colour is assigned deterministically by index in catalog order. */
|
|
7
7
|
const order: string[] = catalog.contexts.map((c) => c.id);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The fields the GitOps tree and the deployer disagree on, said in one line
|
|
2
|
+
// each. Read by the deployment rows on a service's page and by the
|
|
3
|
+
// `deployment` subject a rule sees, so the two say the same words.
|
|
4
|
+
|
|
5
|
+
import type { Deployment } from "../catalog";
|
|
6
|
+
|
|
7
|
+
export function driftLines(deployment: Deployment): string[] {
|
|
8
|
+
const drift = deployment.drift;
|
|
9
|
+
if (!drift) return [];
|
|
10
|
+
const lines: string[] = [];
|
|
11
|
+
if (drift.project) lines.push(`project ${drift.project} in the tree, ${deployment.project} deployed`);
|
|
12
|
+
if (drift.cluster) lines.push(`cluster ${drift.cluster} in the tree, ${deployment.cluster} deployed`);
|
|
13
|
+
if (drift.namespace) lines.push(`namespace ${drift.namespace} in the tree, ${deployment.namespace} deployed`);
|
|
14
|
+
if (drift.path) lines.push(`path ${drift.path} in the tree, ${deployment.path} deployed`);
|
|
15
|
+
if (drift.targetRevision) {
|
|
16
|
+
lines.push(`tracks ${drift.targetRevision} in the tree, ${deployment.targetRevision} deployed`);
|
|
17
|
+
}
|
|
18
|
+
if (drift.images?.length) {
|
|
19
|
+
lines.push(`pins ${drift.images.join(", ")} in the tree; running ${(deployment.images ?? []).join(", ") || "nothing"}`);
|
|
20
|
+
}
|
|
21
|
+
return lines;
|
|
22
|
+
}
|