@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
|
@@ -12,10 +12,15 @@
|
|
|
12
12
|
// is either a publisher outside the estate or a name that has drifted, and the
|
|
13
13
|
// Problems page says which; a row that quietly dropped it would be the site
|
|
14
14
|
// hiding the interesting case.
|
|
15
|
+
//
|
|
16
|
+
// Direction is the first thing a reader asks of a channel - what goes out, what
|
|
17
|
+
// comes in - so it is drawn, not only written: an arrow and a colour per
|
|
18
|
+
// direction, a filter to keep one, and a grouping that folds the two rows a
|
|
19
|
+
// message has on a work queue (put on, worked) into one.
|
|
15
20
|
|
|
16
|
-
import { ExternalLink } from "lucide-react";
|
|
17
|
-
import { Link } from "react-router";
|
|
18
|
-
import type { Channel, ChannelMessage } from "../catalog";
|
|
21
|
+
import { ArrowDownLeft, ArrowUpRight, ExternalLink, Layers } from "lucide-react";
|
|
22
|
+
import { Link, useSearchParams } from "react-router";
|
|
23
|
+
import type { Channel, ChannelDirection, ChannelMessage } from "../catalog";
|
|
19
24
|
import { catalog, index } from "../data";
|
|
20
25
|
import {
|
|
21
26
|
isKafkaChannel,
|
|
@@ -29,6 +34,8 @@ import { RowActions } from "./RowActions";
|
|
|
29
34
|
|
|
30
35
|
const KAFKA_HANDOFF_CHANNELS = kafkaHandoffChannels(catalog);
|
|
31
36
|
|
|
37
|
+
export type DirectionFilter = "all" | ChannelDirection;
|
|
38
|
+
|
|
32
39
|
/** The event that goes out under a wire name, when the catalog knows one. */
|
|
33
40
|
function publisherOf(name: string) {
|
|
34
41
|
const event = index.eventByWireName.get(name);
|
|
@@ -40,11 +47,41 @@ function publisherOf(name: string) {
|
|
|
40
47
|
return { event, owner, path };
|
|
41
48
|
}
|
|
42
49
|
|
|
50
|
+
const DIRECTION = {
|
|
51
|
+
send: {
|
|
52
|
+
Icon: ArrowUpRight,
|
|
53
|
+
className: "chip dir-send",
|
|
54
|
+
title: "this service puts it on the channel",
|
|
55
|
+
},
|
|
56
|
+
receive: {
|
|
57
|
+
Icon: ArrowDownLeft,
|
|
58
|
+
className: "chip dir-receive",
|
|
59
|
+
title: "this service listens for it",
|
|
60
|
+
},
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
function DirectionChip({ direction }: { direction: ChannelDirection }) {
|
|
64
|
+
const { Icon, className, title } = DIRECTION[direction];
|
|
65
|
+
return (
|
|
66
|
+
<span className={className} title={title} data-direction={direction}>
|
|
67
|
+
<Icon size={11} aria-hidden />
|
|
68
|
+
{direction}
|
|
69
|
+
</span>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* One row: a message and the directions it travels. Ungrouped, a message that
|
|
75
|
+
* is both sent and received is two rows with one direction each; grouped, it
|
|
76
|
+
* is one row wearing both chips.
|
|
77
|
+
*/
|
|
43
78
|
function MessageRow({
|
|
44
79
|
message,
|
|
80
|
+
directions,
|
|
45
81
|
mine,
|
|
46
82
|
}: {
|
|
47
83
|
message: ChannelMessage;
|
|
84
|
+
directions: readonly ChannelDirection[];
|
|
48
85
|
/** The service this channel belongs to, so a row can say when the publisher is someone else. */
|
|
49
86
|
mine: string;
|
|
50
87
|
}) {
|
|
@@ -58,16 +95,9 @@ function MessageRow({
|
|
|
58
95
|
<li className="row rounded-none border-x-0 border-t-0 last:border-b-0">
|
|
59
96
|
<div className="min-w-0 flex-1">
|
|
60
97
|
<div className="flex flex-wrap items-center gap-x-2">
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
message.direction === "send"
|
|
65
|
-
? "this service puts it on the channel"
|
|
66
|
-
: "this service listens for it"
|
|
67
|
-
}
|
|
68
|
-
>
|
|
69
|
-
{message.direction}
|
|
70
|
-
</span>
|
|
98
|
+
{directions.map((direction) => (
|
|
99
|
+
<DirectionChip key={direction} direction={direction} />
|
|
100
|
+
))}
|
|
71
101
|
{published ? (
|
|
72
102
|
<Link
|
|
73
103
|
to={published.path}
|
|
@@ -99,6 +129,64 @@ function MessageRow({
|
|
|
99
129
|
);
|
|
100
130
|
}
|
|
101
131
|
|
|
132
|
+
/** A message with every direction it travels on the channel, in send-then-receive order. */
|
|
133
|
+
interface MessageGroup {
|
|
134
|
+
message: ChannelMessage;
|
|
135
|
+
directions: ChannelDirection[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The rows a channel shows: one per message and direction, or - grouped - one
|
|
140
|
+
* per message with both directions, the sent one supplying the doc because it
|
|
141
|
+
* is what the sender wrote down. A filter keeps one direction; a grouped row
|
|
142
|
+
* that loses its other half keeps the chip it still has.
|
|
143
|
+
*/
|
|
144
|
+
export function rowsOf(
|
|
145
|
+
messages: readonly ChannelMessage[],
|
|
146
|
+
filter: DirectionFilter,
|
|
147
|
+
grouped: boolean,
|
|
148
|
+
): MessageGroup[] {
|
|
149
|
+
const kept = messages.filter(
|
|
150
|
+
(message) => filter === "all" || message.direction === filter,
|
|
151
|
+
);
|
|
152
|
+
if (!grouped) {
|
|
153
|
+
return kept.map((message) => ({ message, directions: [message.direction] }));
|
|
154
|
+
}
|
|
155
|
+
const groups: MessageGroup[] = [];
|
|
156
|
+
for (const message of kept) {
|
|
157
|
+
const group = groups.find((g) => g.message.name === message.name);
|
|
158
|
+
if (!group) {
|
|
159
|
+
groups.push({ message, directions: [message.direction] });
|
|
160
|
+
} else {
|
|
161
|
+
if (!group.directions.includes(message.direction)) {
|
|
162
|
+
group.directions.push(message.direction);
|
|
163
|
+
}
|
|
164
|
+
// The sent message carries the sender's doc; a receive found first is
|
|
165
|
+
// replaced by it, so that a grouped row reads the same either way.
|
|
166
|
+
if (message.direction === "send" && group.message.direction === "receive") {
|
|
167
|
+
group.message = message;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const group of groups) {
|
|
172
|
+
group.directions.sort((a, b) => (a === b ? 0 : a === "send" ? -1 : 1));
|
|
173
|
+
}
|
|
174
|
+
return groups;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** How many messages travel each way, across every channel. */
|
|
178
|
+
export function countDirections(channels: readonly Channel[]) {
|
|
179
|
+
let send = 0;
|
|
180
|
+
let receive = 0;
|
|
181
|
+
for (const channel of channels) {
|
|
182
|
+
for (const message of channel.messages) {
|
|
183
|
+
if (message.direction === "send") send += 1;
|
|
184
|
+
else receive += 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { send, receive };
|
|
188
|
+
}
|
|
189
|
+
|
|
102
190
|
export function ChannelRows({
|
|
103
191
|
channels,
|
|
104
192
|
service,
|
|
@@ -107,12 +195,75 @@ export function ChannelRows({
|
|
|
107
195
|
service: string;
|
|
108
196
|
}) {
|
|
109
197
|
const kafkaUi = useKafkaUi((state) => state.url);
|
|
198
|
+
// The filter and the grouping live in the URL beside the tab, so that the
|
|
199
|
+
// view a reader ends up with is the one their link opens.
|
|
200
|
+
const [params, setParams] = useSearchParams();
|
|
201
|
+
const dir = params.get("dir");
|
|
202
|
+
const filter: DirectionFilter =
|
|
203
|
+
dir === "send" || dir === "receive" ? dir : "all";
|
|
204
|
+
const grouped = params.get("group") === "message";
|
|
205
|
+
const set = (key: string, value: string | null) => {
|
|
206
|
+
const next = new URLSearchParams(params);
|
|
207
|
+
if (value === null) next.delete(key);
|
|
208
|
+
else next.set(key, value);
|
|
209
|
+
setParams(next, { replace: true });
|
|
210
|
+
};
|
|
211
|
+
const counts = countDirections(channels);
|
|
212
|
+
|
|
110
213
|
return (
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
214
|
+
<div className="flex flex-col gap-3">
|
|
215
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
216
|
+
<div className="seg bg-canvas" role="group" aria-label="Direction shown">
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
onClick={() => set("dir", null)}
|
|
220
|
+
aria-pressed={filter === "all"}
|
|
221
|
+
className={filter === "all" ? "is-on" : ""}
|
|
222
|
+
title="Every message, both ways"
|
|
223
|
+
>
|
|
224
|
+
all <span className="tnum text-muted">{counts.send + counts.receive}</span>
|
|
225
|
+
</button>
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
onClick={() => set("dir", "send")}
|
|
229
|
+
aria-pressed={filter === "send"}
|
|
230
|
+
className={filter === "send" ? "is-on" : ""}
|
|
231
|
+
title="Only what this service puts on a channel"
|
|
232
|
+
>
|
|
233
|
+
<ArrowUpRight size={11} aria-hidden className="inline" /> send{" "}
|
|
234
|
+
<span className="tnum text-muted">{counts.send}</span>
|
|
235
|
+
</button>
|
|
236
|
+
<button
|
|
237
|
+
type="button"
|
|
238
|
+
onClick={() => set("dir", "receive")}
|
|
239
|
+
aria-pressed={filter === "receive"}
|
|
240
|
+
className={filter === "receive" ? "is-on" : ""}
|
|
241
|
+
title="Only what this service listens for"
|
|
242
|
+
>
|
|
243
|
+
<ArrowDownLeft size={11} aria-hidden className="inline" /> receive{" "}
|
|
244
|
+
<span className="tnum text-muted">{counts.receive}</span>
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
<div className="seg bg-canvas" role="group" aria-label="Grouping">
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
onClick={() => set("group", grouped ? null : "message")}
|
|
251
|
+
aria-pressed={grouped}
|
|
252
|
+
className={grouped ? "is-on" : ""}
|
|
253
|
+
title="One row per message, with every direction it travels"
|
|
254
|
+
>
|
|
255
|
+
<Layers size={11} aria-hidden className="inline" /> by message
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
<ChannelRowsContent
|
|
260
|
+
channels={channels}
|
|
261
|
+
service={service}
|
|
262
|
+
kafkaUi={kafkaUi}
|
|
263
|
+
filter={filter}
|
|
264
|
+
grouped={grouped}
|
|
265
|
+
/>
|
|
266
|
+
</div>
|
|
116
267
|
);
|
|
117
268
|
}
|
|
118
269
|
|
|
@@ -121,59 +272,71 @@ export function ChannelRowsContent({
|
|
|
121
272
|
channels,
|
|
122
273
|
service,
|
|
123
274
|
kafkaUi,
|
|
275
|
+
filter = "all",
|
|
276
|
+
grouped = false,
|
|
124
277
|
}: {
|
|
125
278
|
channels: Channel[];
|
|
126
279
|
service: string;
|
|
127
280
|
kafkaUi: string;
|
|
281
|
+
filter?: DirectionFilter;
|
|
282
|
+
grouped?: boolean;
|
|
128
283
|
}) {
|
|
129
284
|
return (
|
|
130
285
|
<div className="flex flex-col gap-section" data-nav-list>
|
|
131
|
-
{channels.map((channel) =>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
286
|
+
{channels.map((channel) => {
|
|
287
|
+
const rows = rowsOf(channel.messages, filter, grouped);
|
|
288
|
+
return (
|
|
289
|
+
<div key={channel.address} className="rounded-card border border-line">
|
|
290
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 border-b border-line px-3 py-2">
|
|
291
|
+
<div className="flex min-w-0 flex-1 flex-wrap items-baseline gap-x-2">
|
|
292
|
+
<Ident value={channel.address} />
|
|
293
|
+
{channel.kind === "job" ? (
|
|
294
|
+
<span className="chip">work queue</span>
|
|
295
|
+
) : channel.kind === "message" ? (
|
|
296
|
+
<span className="chip">message stream</span>
|
|
297
|
+
) : null}
|
|
298
|
+
{channel.title ? (
|
|
299
|
+
<span className="text-muted">{channel.title}</span>
|
|
300
|
+
) : null}
|
|
301
|
+
</div>
|
|
302
|
+
{kafkaUi && isKafkaChannel(channel, KAFKA_HANDOFF_CHANNELS) ? (
|
|
303
|
+
<a
|
|
304
|
+
href={kafkaUiTopicUrl(kafkaUi, channel.address) ?? kafkaUi}
|
|
305
|
+
target="_blank"
|
|
306
|
+
rel="noreferrer"
|
|
307
|
+
className="mono inline-flex shrink-0 items-center gap-1 whitespace-nowrap rounded-control text-accent hover:underline"
|
|
308
|
+
title={`Open ${channel.address} in Kafka UI`}
|
|
309
|
+
>
|
|
310
|
+
view in Kafka UI <ExternalLink size={12} aria-hidden />
|
|
311
|
+
</a>
|
|
143
312
|
) : null}
|
|
144
313
|
</div>
|
|
145
|
-
{
|
|
146
|
-
<
|
|
147
|
-
href={kafkaUiTopicUrl(kafkaUi, channel.address) ?? kafkaUi}
|
|
148
|
-
target="_blank"
|
|
149
|
-
rel="noreferrer"
|
|
150
|
-
className="mono inline-flex shrink-0 items-center gap-1 whitespace-nowrap rounded-control text-accent hover:underline"
|
|
151
|
-
title={`Open ${channel.address} in Kafka UI`}
|
|
152
|
-
>
|
|
153
|
-
view in Kafka UI <ExternalLink size={12} aria-hidden />
|
|
154
|
-
</a>
|
|
314
|
+
{channel.doc ? (
|
|
315
|
+
<p className="px-3 py-2 text-muted">{channel.doc}</p>
|
|
155
316
|
) : null}
|
|
317
|
+
{channel.messages.length === 0 ? (
|
|
318
|
+
<p className="px-3 py-2 text-muted">
|
|
319
|
+
declared, and no operation says which way it travels
|
|
320
|
+
</p>
|
|
321
|
+
) : rows.length === 0 ? (
|
|
322
|
+
<p className="px-3 py-2 text-muted">
|
|
323
|
+
{filter === "send" ? "nothing sent" : "nothing received"}
|
|
324
|
+
</p>
|
|
325
|
+
) : (
|
|
326
|
+
<ul>
|
|
327
|
+
{rows.map(({ message, directions }) => (
|
|
328
|
+
<MessageRow
|
|
329
|
+
key={`${directions.join("+")} ${message.name}`}
|
|
330
|
+
message={message}
|
|
331
|
+
directions={directions}
|
|
332
|
+
mine={service}
|
|
333
|
+
/>
|
|
334
|
+
))}
|
|
335
|
+
</ul>
|
|
336
|
+
)}
|
|
156
337
|
</div>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
) : null}
|
|
160
|
-
{channel.messages.length === 0 ? (
|
|
161
|
-
<p className="px-3 py-2 text-muted">
|
|
162
|
-
declared, and no operation says which way it travels
|
|
163
|
-
</p>
|
|
164
|
-
) : (
|
|
165
|
-
<ul>
|
|
166
|
-
{channel.messages.map((message) => (
|
|
167
|
-
<MessageRow
|
|
168
|
-
key={`${message.direction} ${message.name}`}
|
|
169
|
-
message={message}
|
|
170
|
-
mine={service}
|
|
171
|
-
/>
|
|
172
|
-
))}
|
|
173
|
-
</ul>
|
|
174
|
-
)}
|
|
175
|
-
</div>
|
|
176
|
-
))}
|
|
338
|
+
);
|
|
339
|
+
})}
|
|
177
340
|
</div>
|
|
178
341
|
);
|
|
179
342
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Where a service runs, one row per Application the deployer lists for it.
|
|
2
|
+
//
|
|
3
|
+
// The environment is the row's identity: it is what a reader scans for -
|
|
4
|
+
// "is it in prod yet" - and what the snapshot groups by. The revision is the
|
|
5
|
+
// answer to the next question, "which commit", and is a link to the commit
|
|
6
|
+
// on the forge when it is one; a chart version is not, and is shown as it
|
|
7
|
+
// is. The application link opens the deployer's own page, which is where
|
|
8
|
+
// health and sync live: they move without a commit and are not kept here.
|
|
9
|
+
|
|
10
|
+
import { ExternalLink } from "lucide-react";
|
|
11
|
+
import type { Deployment } from "../catalog";
|
|
12
|
+
import { deploymentBasis } from "../catalog";
|
|
13
|
+
import { driftLines } from "../lib/deployment-drift";
|
|
14
|
+
import { Ident } from "./Ident";
|
|
15
|
+
import { RowActions } from "./RowActions";
|
|
16
|
+
|
|
17
|
+
/** The commit on the forge, when the revision is one and the repository is known. */
|
|
18
|
+
function commitHref(deployment: Deployment): string | null {
|
|
19
|
+
if (!deployment.repo || !/^[0-9a-f]{40}$/.test(deployment.revision)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const repo = `https://${deployment.repo}`;
|
|
23
|
+
const view = repo.toLowerCase().includes("gitlab") ? "/-/commit/" : "/commit/";
|
|
24
|
+
return `${repo}${view}${deployment.revision}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A commit shortened the way `git log --oneline` shortens it; anything else whole. */
|
|
28
|
+
function shortRevision(revision: string): string {
|
|
29
|
+
return /^[0-9a-f]{40}$/.test(revision) ? revision.slice(0, 7) : revision;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function DeploymentRow({ deployment }: { deployment: Deployment }) {
|
|
33
|
+
const href = commitHref(deployment);
|
|
34
|
+
const where = [deployment.cluster, deployment.namespace]
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join(" / ");
|
|
37
|
+
return (
|
|
38
|
+
<div className="row items-start px-2 py-1.5">
|
|
39
|
+
<span className="chip" title="the environment the application is placed in">
|
|
40
|
+
{deployment.environment}
|
|
41
|
+
</span>
|
|
42
|
+
<span className="mono text-muted" title="cluster / namespace">
|
|
43
|
+
{where}
|
|
44
|
+
</span>
|
|
45
|
+
<div className="min-w-0">
|
|
46
|
+
<span className="inline-flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
47
|
+
{deployment.revision ? (
|
|
48
|
+
href ? (
|
|
49
|
+
<a
|
|
50
|
+
href={href}
|
|
51
|
+
target="_blank"
|
|
52
|
+
rel="noreferrer"
|
|
53
|
+
className="mono text-ink hover:underline"
|
|
54
|
+
title={`${deployment.revision} on the forge`}
|
|
55
|
+
>
|
|
56
|
+
{shortRevision(deployment.revision)}
|
|
57
|
+
</a>
|
|
58
|
+
) : (
|
|
59
|
+
<Ident value={deployment.revision}>
|
|
60
|
+
{shortRevision(deployment.revision)}
|
|
61
|
+
</Ident>
|
|
62
|
+
)
|
|
63
|
+
) : (
|
|
64
|
+
<span className="text-muted">not synced yet</span>
|
|
65
|
+
)}
|
|
66
|
+
{deployment.targetRevision ? (
|
|
67
|
+
<span className="text-muted" title="what the application tracks">
|
|
68
|
+
tracks {deployment.targetRevision}
|
|
69
|
+
</span>
|
|
70
|
+
) : null}
|
|
71
|
+
{deployment.tool ? (
|
|
72
|
+
<span className="chip mono">{deployment.tool}</span>
|
|
73
|
+
) : null}
|
|
74
|
+
{/* Who said so, when only the tree has: what should run, with
|
|
75
|
+
nothing yet saying it does. A row both spoke for says nothing
|
|
76
|
+
here - agreement is the ordinary case and needs no chip. */}
|
|
77
|
+
{deploymentBasis(deployment) === "manifest" ? (
|
|
78
|
+
<span
|
|
79
|
+
className="chip status-declared"
|
|
80
|
+
title="declared in the GitOps tree; the deployer has not been read for it"
|
|
81
|
+
>
|
|
82
|
+
declared
|
|
83
|
+
</span>
|
|
84
|
+
) : null}
|
|
85
|
+
{/* The tree and the deployer disagree. The chip carries the
|
|
86
|
+
difference in words, so a reader does not have to open both. */}
|
|
87
|
+
{driftLines(deployment).length > 0 ? (
|
|
88
|
+
<span
|
|
89
|
+
className="chip status-unresolved"
|
|
90
|
+
title={driftLines(deployment).join("\n")}
|
|
91
|
+
>
|
|
92
|
+
drift
|
|
93
|
+
</span>
|
|
94
|
+
) : null}
|
|
95
|
+
</span>
|
|
96
|
+
{deployment.images?.length ? (
|
|
97
|
+
<ul className="mt-0.5 flex flex-wrap gap-x-2 gap-y-0.5">
|
|
98
|
+
{deployment.images.map((image) => (
|
|
99
|
+
<li key={image}>
|
|
100
|
+
<Ident value={image} className="text-muted">
|
|
101
|
+
{image}
|
|
102
|
+
</Ident>
|
|
103
|
+
</li>
|
|
104
|
+
))}
|
|
105
|
+
</ul>
|
|
106
|
+
) : null}
|
|
107
|
+
</div>
|
|
108
|
+
<a
|
|
109
|
+
href={deployment.url}
|
|
110
|
+
target="_blank"
|
|
111
|
+
rel="noreferrer"
|
|
112
|
+
className="inline-flex items-center gap-1 rounded-control text-accent hover:underline"
|
|
113
|
+
title={`${deployment.name} in Argo CD, where its health and sync state are`}
|
|
114
|
+
>
|
|
115
|
+
{deployment.name}
|
|
116
|
+
<ExternalLink size={12} aria-hidden />
|
|
117
|
+
</a>
|
|
118
|
+
<RowActions copy={deployment.name} label={deployment.name} />
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function DeploymentRows({ deployments }: { deployments: Deployment[] }) {
|
|
124
|
+
return (
|
|
125
|
+
<div className="rows grid-cols-[auto_auto_1fr_auto_auto]">
|
|
126
|
+
{deployments.map((deployment) => (
|
|
127
|
+
<DeploymentRow key={deployment.id} deployment={deployment} />
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { DocsLinksContent } from "./DocsLinks";
|
|
4
|
+
|
|
5
|
+
describe("DocsLinks", () => {
|
|
6
|
+
it("searches Confluence by name and opens the Notion workspace", () => {
|
|
7
|
+
const markup = renderToStaticMarkup(
|
|
8
|
+
<DocsLinksContent
|
|
9
|
+
name="shop.oms"
|
|
10
|
+
confluence="https://acme.atlassian.net/wiki/spaces/ARCH"
|
|
11
|
+
notion="https://www.notion.so/acme"
|
|
12
|
+
/>,
|
|
13
|
+
);
|
|
14
|
+
expect(markup).toContain("view in Confluence");
|
|
15
|
+
expect(markup).toContain(
|
|
16
|
+
'href="https://acme.atlassian.net/wiki/search?text=shop.oms&spaces=ARCH"',
|
|
17
|
+
);
|
|
18
|
+
expect(markup).toContain("open Notion");
|
|
19
|
+
expect(markup).toContain('href="https://www.notion.so/acme"');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("renders nothing before either integration is configured", () => {
|
|
23
|
+
expect(
|
|
24
|
+
renderToStaticMarkup(<DocsLinksContent name="shop.oms" confluence="" notion="" />),
|
|
25
|
+
).toBe("");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The hand-off from a page to where its team writes: a Confluence search by
|
|
2
|
+
// name, and the Notion workspace. Nothing until the reader has configured
|
|
3
|
+
// either under Settings → Integrations; the values live in this browser.
|
|
4
|
+
|
|
5
|
+
import { ExternalLink } from "lucide-react";
|
|
6
|
+
import { confluenceSearchUrl, useConfluence } from "../lib/confluence";
|
|
7
|
+
import { useNotion } from "../lib/notion";
|
|
8
|
+
|
|
9
|
+
const LINK =
|
|
10
|
+
"inline-flex items-center gap-1 whitespace-nowrap rounded-control text-accent hover:underline";
|
|
11
|
+
|
|
12
|
+
export function DocsLinksContent({
|
|
13
|
+
name,
|
|
14
|
+
confluence,
|
|
15
|
+
notion,
|
|
16
|
+
}: {
|
|
17
|
+
/** What to search for: the service's or context's name. */
|
|
18
|
+
name: string;
|
|
19
|
+
confluence: string;
|
|
20
|
+
notion: string;
|
|
21
|
+
}) {
|
|
22
|
+
const search = confluenceSearchUrl(confluence, name);
|
|
23
|
+
if (!search && !notion) return null;
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
{search ? (
|
|
27
|
+
<a
|
|
28
|
+
href={search}
|
|
29
|
+
target="_blank"
|
|
30
|
+
rel="noreferrer"
|
|
31
|
+
className={LINK}
|
|
32
|
+
title={`Search Confluence for ${name}`}
|
|
33
|
+
>
|
|
34
|
+
view in Confluence <ExternalLink size={12} aria-hidden />
|
|
35
|
+
</a>
|
|
36
|
+
) : null}
|
|
37
|
+
{notion ? (
|
|
38
|
+
<a
|
|
39
|
+
href={notion}
|
|
40
|
+
target="_blank"
|
|
41
|
+
rel="noreferrer"
|
|
42
|
+
className={LINK}
|
|
43
|
+
title="Open the Notion workspace where the documentation lives"
|
|
44
|
+
>
|
|
45
|
+
open Notion <ExternalLink size={12} aria-hidden />
|
|
46
|
+
</a>
|
|
47
|
+
) : null}
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function DocsLinks({ name }: { name: string }) {
|
|
53
|
+
const confluence = useConfluence((state) => state.url);
|
|
54
|
+
const notion = useNotion((state) => state.url);
|
|
55
|
+
return <DocsLinksContent name={name} confluence={confluence} notion={notion} />;
|
|
56
|
+
}
|
|
@@ -22,6 +22,7 @@ import type { Scope, Shape } from "../lib/shape";
|
|
|
22
22
|
import { blockPath, enumPath } from "../routes";
|
|
23
23
|
import { Ident } from "./Ident";
|
|
24
24
|
import { KindIcon } from "./kind";
|
|
25
|
+
import { RuleMarks } from "./RuleMarks";
|
|
25
26
|
|
|
26
27
|
/** The words the type chip uses for what `parseType` found. */
|
|
27
28
|
const CARDINALITY: Record<"many" | "map", string> = {
|
|
@@ -31,8 +32,9 @@ const CARDINALITY: Record<"many" | "map", string> = {
|
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* The type of a field, as a reader wants it: the raw spelling to copy, a
|
|
34
|
-
* word for the wrapper it is in,
|
|
35
|
-
*
|
|
35
|
+
* word for the wrapper it is in, what the source says the value must
|
|
36
|
+
* satisfy, and a way to the page of the shape it names. Shared by the table
|
|
37
|
+
* cell and the tree row so both say it the same.
|
|
36
38
|
*/
|
|
37
39
|
export function TypeCell({
|
|
38
40
|
field,
|
|
@@ -70,6 +72,7 @@ export function TypeCell({
|
|
|
70
72
|
optional
|
|
71
73
|
</span>
|
|
72
74
|
) : null}
|
|
75
|
+
<RuleMarks field={field} />
|
|
73
76
|
{page && shape ? (
|
|
74
77
|
<Link
|
|
75
78
|
to={page}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import { HTTPDestinationEvidence } from "./HTTPDestinationEvidence";
|
|
4
|
+
|
|
5
|
+
it("shows the join and the config default behind a resolved destination", () => {
|
|
6
|
+
const html = renderToStaticMarkup(<HTTPDestinationEvidence destination={{
|
|
7
|
+
callSite: "client.go:10", endpointExpression: "c.baseURL + path", method: "POST",
|
|
8
|
+
localPath: "/get-admin-settings", fullPath: "/settings/get-admin-settings",
|
|
9
|
+
baseURL: { expression: "cfg.SettingAddr", configField: "Config.SettingAddr", environmentVariable: "SETTINGS_ADDR", kind: "config-default", value: "http://localhost:8000/settings", source: "config.go:84", optionSource: "modules_tree.go:212" },
|
|
10
|
+
join: { expression: "c.baseURL + path", source: "client.go:10" },
|
|
11
|
+
resolution: { basis: "full-path", provider: "avia.aviaadmin", route: "/settings/get-admin-settings" },
|
|
12
|
+
}} />);
|
|
13
|
+
for (const value of ["Recovered full path", "Config default (runtime may override)", "SETTINGS_ADDR", "modules_tree.go:212", "Config.SettingAddr", "/settings/get-admin-settings", "avia.aviaadmin"]) expect(html).toContain(value);
|
|
14
|
+
expect(html).not.toContain("heuristic");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("labels suffix resolution as a heuristic", () => {
|
|
18
|
+
const html = renderToStaticMarkup(<HTTPDestinationEvidence destination={{
|
|
19
|
+
callSite: "client.go:10", endpointExpression: "/settings", method: "POST",
|
|
20
|
+
resolution: { basis: "unique-suffix", provider: "admin", route: "/admin/settings" },
|
|
21
|
+
}} />);
|
|
22
|
+
expect(html).toContain("Unique route suffix (heuristic)");
|
|
23
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { HTTPDestination } from "../catalog";
|
|
2
|
+
|
|
3
|
+
/** Display the facts for this invocation, including the limits of defaults. */
|
|
4
|
+
export function HTTPDestinationEvidence({ destination: d }: { destination: HTTPDestination }) {
|
|
5
|
+
const basis = d.resolution?.basis;
|
|
6
|
+
const rows = [
|
|
7
|
+
["Linked by", basis === "full-path" ? "Recovered full path" : basis === "exact-route" ? "Exact route" : basis === "unique-suffix" ? "Unique route suffix (heuristic)" : basis === "kubernetes-host" ? "Kubernetes Service host, from the manifests" : "Unresolved"],
|
|
8
|
+
["Call site", d.callSite],
|
|
9
|
+
["Endpoint expression", d.endpointExpression],
|
|
10
|
+
["Local path", d.localPath],
|
|
11
|
+
["Base URL expression", d.baseURL?.expression],
|
|
12
|
+
["Config field", d.baseURL?.configField],
|
|
13
|
+
["Environment variable", d.baseURL?.environmentVariable],
|
|
14
|
+
[d.baseURL?.kind === "config-default" ? "Config default (runtime may override)" : "Base URL", d.baseURL?.value],
|
|
15
|
+
["Config source", d.baseURL?.source],
|
|
16
|
+
["Option call site", d.baseURL?.optionSource],
|
|
17
|
+
["Service discovery alias", d.serviceDiscoveryAlias],
|
|
18
|
+
["Join expression", d.join?.expression],
|
|
19
|
+
["Join source", d.join?.source],
|
|
20
|
+
["Full path", d.fullPath],
|
|
21
|
+
...(d.transforms ?? []).map((transform) => ["Runtime URL modifier (not evaluated)", `${transform.expression} · ${transform.source}`]),
|
|
22
|
+
["Provider", d.resolution?.provider],
|
|
23
|
+
["Provider route", d.resolution?.route],
|
|
24
|
+
].filter(([, value]) => value);
|
|
25
|
+
return <section aria-label="HTTP destination evidence" className="border-t border-line px-3 py-3">
|
|
26
|
+
<h3 className="label mb-2">HTTP destination evidence</h3>
|
|
27
|
+
<dl className="grid min-w-0 grid-cols-1 gap-y-3">
|
|
28
|
+
{rows.map(([label, value], index) => <div key={`${label}-${index}`} className="min-w-0"><dt className="text-muted">{label}</dt><dd className="mono break-all text-ink">{value}</dd></div>)}
|
|
29
|
+
</dl>
|
|
30
|
+
</section>;
|
|
31
|
+
}
|
|
@@ -210,7 +210,7 @@ export function Integrations({ groups }: { groups: IntegrationGroup[] }) {
|
|
|
210
210
|
<RowActions copy={call.id} label={call.id} />
|
|
211
211
|
{method?.http ? (
|
|
212
212
|
<p className="mono w-full text-muted">
|
|
213
|
-
{method.http.method} {method.http.path}
|
|
213
|
+
{method.http.method ? `${method.http.method} ${method.http.path}` : method.http.path}
|
|
214
214
|
</p>
|
|
215
215
|
) : null}
|
|
216
216
|
{call.note ? (
|