@shortlink-org/portolan 0.2.3 → 0.2.4
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/package.json +1 -1
- package/plugins/README.md +5 -0
- package/plugins/extract-python-kafka/README.md +6 -0
- package/plugins/extract-python-kafka/extract.py +2 -2
- package/plugins/extract-python-kafka/extract_test.py +17 -1
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/catalog.py +12 -1
- package/plugins/pyplugin/kafka.py +74 -3
- package/scripts/gen-likec4.mjs +78 -20
- package/scripts/gen-likec4.test.mjs +25 -2
- package/src/app/Breadcrumbs.test.ts +4 -0
- package/src/app/Breadcrumbs.tsx +1 -0
- package/src/catalog-model.ts +22 -1
- package/src/catalog-stores.test.ts +17 -0
- package/src/catalog-validation.ts +43 -2
- package/src/catalog.test.ts +13 -2
- package/src/components/ChannelRows.messagepack.test.tsx +28 -0
- package/src/components/ChannelRows.test.tsx +54 -0
- package/src/components/ChannelRows.tsx +57 -10
- package/src/components/LifecycleDiagram.tsx +28 -12
- package/src/components/ProblemRow.tsx +4 -0
- package/src/components/WhatLinksHere.tsx +6 -4
- package/src/enrich.test.ts +4 -5
- package/src/flow/StepDetail.tsx +98 -54
- package/src/flow/answers.test.ts +18 -1
- package/src/flow/answers.ts +37 -8
- package/src/lib/backlinks.test.ts +16 -1
- package/src/lib/backlinks.ts +20 -0
- package/src/lib/catalog-diff.test.ts +18 -0
- package/src/lib/catalog-diff.ts +19 -1
- package/src/lib/derive.ts +1 -0
- package/src/lib/kafka-ui.test.ts +87 -0
- package/src/lib/kafka-ui.ts +105 -0
- package/src/lib/wire-problems.test.ts +21 -0
- package/src/lib/wire-problems.ts +62 -1
- package/src/likec4/FlowView.tsx +2 -6
- package/src/likec4/flow-edges.test.ts +64 -1
- package/src/likec4/flow-edges.ts +43 -7
- package/src/likec4/view-index.ts +8 -2
- package/src/merge.test.ts +23 -0
- package/src/merge.ts +17 -1
- package/src/pages/CatalogFailure.tsx +2 -2
- package/src/pages/Settings.tsx +11 -3
- package/src/pages/settings/IntegrationsSettings.tsx +117 -0
- package/src/routes.test.ts +2 -0
- package/src/routes.ts +2 -1
- package/src/selection/DetailPanel.tsx +46 -1
package/src/likec4/flow-edges.ts
CHANGED
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
// id across, so the two are paired by position.
|
|
4
4
|
//
|
|
5
5
|
// That is sound because it is the same walk twice: the generator emits steps in
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// abandoned rather than guessed at, and the diagram simply stops highlighting.
|
|
6
|
+
// catalog order and this module inserts the same contract-response edges in the
|
|
7
|
+
// same places. If the two lists ever differ in length the pairing is abandoned
|
|
8
|
+
// rather than guessed at, and the diagram simply stops highlighting.
|
|
10
9
|
|
|
11
10
|
import type { Flow } from "../catalog";
|
|
12
11
|
import { walkSteps } from "../catalog";
|
|
@@ -15,13 +14,16 @@ import { hiddenStepIds } from "../flow/cross-context";
|
|
|
15
14
|
export interface EdgeStepPairing {
|
|
16
15
|
/** LikeC4 edge id -> catalog step id */
|
|
17
16
|
stepOf: Map<string, string>;
|
|
18
|
-
/** catalog step id -> LikeC4 edge id */
|
|
17
|
+
/** catalog step id -> its primary LikeC4 edge id */
|
|
19
18
|
edgeOf: Map<string, string>;
|
|
19
|
+
/** catalog step id -> every edge it draws, including a contract response */
|
|
20
|
+
edgesOf: Map<string, string[]>;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export const EMPTY_PAIRING: EdgeStepPairing = {
|
|
23
24
|
stepOf: new Map(),
|
|
24
25
|
edgeOf: new Map(),
|
|
26
|
+
edgesOf: new Map(),
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
export function pairEdgesToSteps(
|
|
@@ -31,13 +33,17 @@ export function pairEdgesToSteps(
|
|
|
31
33
|
if (edgeIds.length !== stepIds.length) return EMPTY_PAIRING;
|
|
32
34
|
const stepOf = new Map<string, string>();
|
|
33
35
|
const edgeOf = new Map<string, string>();
|
|
36
|
+
const edgesOf = new Map<string, string[]>();
|
|
34
37
|
edgeIds.forEach((edgeId, i) => {
|
|
35
38
|
const stepId = stepIds[i];
|
|
36
39
|
if (stepId === undefined) return;
|
|
37
40
|
stepOf.set(edgeId, stepId);
|
|
38
|
-
edgeOf.set(stepId, edgeId);
|
|
41
|
+
if (!edgeOf.has(stepId)) edgeOf.set(stepId, edgeId);
|
|
42
|
+
const edges = edgesOf.get(stepId) ?? [];
|
|
43
|
+
edges.push(edgeId);
|
|
44
|
+
edgesOf.set(stepId, edges);
|
|
39
45
|
});
|
|
40
|
-
return { stepOf, edgeOf };
|
|
46
|
+
return { stepOf, edgeOf, edgesOf };
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
/**
|
|
@@ -87,3 +93,33 @@ export function drawnStepIds(flow: Flow, crossOnly: boolean): string[] {
|
|
|
87
93
|
const hidden = hiddenStepIds(flow);
|
|
88
94
|
return steps.filter((s) => !hidden.has(s.id)).map((s) => s.id);
|
|
89
95
|
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* One step id per generated edge, including contract responses synthesized by
|
|
99
|
+
* the LikeC4 generator. Nested RPCs return immediately; the actor request that
|
|
100
|
+
* opened the flow returns after its final step.
|
|
101
|
+
*/
|
|
102
|
+
export function drawnEdgeStepIds(
|
|
103
|
+
flow: Flow,
|
|
104
|
+
crossOnly: boolean,
|
|
105
|
+
responseStepIds: ReadonlySet<string>,
|
|
106
|
+
): string[] {
|
|
107
|
+
const steps = walkSteps(flow.steps);
|
|
108
|
+
const actorIds = new Set(
|
|
109
|
+
flow.participants.filter((p) => p.kind === "actor").map((p) => p.id),
|
|
110
|
+
);
|
|
111
|
+
const deferred = steps.find(
|
|
112
|
+
(step) =>
|
|
113
|
+
step.kind === "rpc" &&
|
|
114
|
+
actorIds.has(step.from) &&
|
|
115
|
+
responseStepIds.has(step.id),
|
|
116
|
+
)?.id;
|
|
117
|
+
const drawn = drawnStepIds(flow, crossOnly);
|
|
118
|
+
const out: string[] = [];
|
|
119
|
+
for (const stepId of drawn) {
|
|
120
|
+
out.push(stepId);
|
|
121
|
+
if (stepId !== deferred && responseStepIds.has(stepId)) out.push(stepId);
|
|
122
|
+
}
|
|
123
|
+
if (deferred && drawn.includes(deferred)) out.push(deferred);
|
|
124
|
+
return out;
|
|
125
|
+
}
|
package/src/likec4/view-index.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
import { pickViewBounds } from "likec4/react";
|
|
9
9
|
import { likec4model } from "./generated";
|
|
10
10
|
import type { Flow } from "../catalog";
|
|
11
|
-
import {
|
|
11
|
+
import { index } from "../data";
|
|
12
|
+
import { flowAnswers } from "../flow/answers";
|
|
13
|
+
import { drawnEdgeStepIds, pairEdgesToSteps } from "./flow-edges";
|
|
12
14
|
import type { EdgeStepPairing } from "./flow-edges";
|
|
13
15
|
import { EMPTY_PAIRING } from "./flow-edges";
|
|
14
16
|
import { flowCrossViewId, flowViewId } from "./ids";
|
|
@@ -70,8 +72,12 @@ export function flowPairing(flow: Flow, crossOnly: boolean): EdgeStepPairing {
|
|
|
70
72
|
const viewId = crossOnly ? flowCrossViewId(flow) : flowViewId(flow);
|
|
71
73
|
const cached = pairings.get(viewId);
|
|
72
74
|
if (cached) return cached;
|
|
75
|
+
const responseStepIds = new Set(flowAnswers(index, flow).keys());
|
|
73
76
|
const pairing = shapeOf(viewId).edgeIds.length
|
|
74
|
-
? pairEdgesToSteps(
|
|
77
|
+
? pairEdgesToSteps(
|
|
78
|
+
shapeOf(viewId).edgeIds,
|
|
79
|
+
drawnEdgeStepIds(flow, crossOnly, responseStepIds),
|
|
80
|
+
)
|
|
75
81
|
: EMPTY_PAIRING;
|
|
76
82
|
pairings.set(viewId, pairing);
|
|
77
83
|
return pairing;
|
package/src/merge.test.ts
CHANGED
|
@@ -757,6 +757,29 @@ describe("mergeCatalogs: schema modules", () => {
|
|
|
757
757
|
// came from.
|
|
758
758
|
expect(channels?.[0]?.source).toBe("bus/asyncapi.yaml");
|
|
759
759
|
});
|
|
760
|
+
|
|
761
|
+
it("keeps a message encoding and reports sources that disagree about it", () => {
|
|
762
|
+
const first = context("shop", ["shop.cart"]);
|
|
763
|
+
first.services[0]!.channels = [{
|
|
764
|
+
address: "shop.cart.basket",
|
|
765
|
+
messages: [{ name: "cart.BasketCreated", direction: "send", encoding: "msgpack", contentType: "application/msgpack" }],
|
|
766
|
+
}];
|
|
767
|
+
const second = context("shop", ["shop.cart"]);
|
|
768
|
+
second.services[0]!.channels = [{
|
|
769
|
+
address: "shop.cart.basket",
|
|
770
|
+
messages: [{ name: "cart.BasketCreated", direction: "send", encoding: "json", contentType: "application/json" }],
|
|
771
|
+
}];
|
|
772
|
+
|
|
773
|
+
const merged = mergeCatalogs([
|
|
774
|
+
source("a-msgpack.json", { contexts: [first] }),
|
|
775
|
+
source("b-json.json", { contexts: [second] }),
|
|
776
|
+
]);
|
|
777
|
+
const message = merged.catalog.contexts[0]!.services[0]!.channels![0]!.messages[0]!;
|
|
778
|
+
expect(message.encoding).toBe("msgpack");
|
|
779
|
+
expect(message.contentType).toBe("application/msgpack");
|
|
780
|
+
expect(merged.conflicts.map((conflict) => conflict.message).join(" ")).toMatch(/encoding json.*msgpack/);
|
|
781
|
+
expect(merged.conflicts.map((conflict) => conflict.message).join(" ")).toMatch(/contentType application\/json.*application\/msgpack/);
|
|
782
|
+
});
|
|
760
783
|
});
|
|
761
784
|
|
|
762
785
|
describe("a second source that has seen the flow run", () => {
|
package/src/merge.ts
CHANGED
|
@@ -442,6 +442,7 @@ function foldMaps(catalog: Catalog, stores: Store[]): Store[] {
|
|
|
442
442
|
if (!aggregate) return table;
|
|
443
443
|
return {
|
|
444
444
|
...table,
|
|
445
|
+
...(table.accesses ? { accesses: table.accesses.map((access) => ({ ...access })) } : {}),
|
|
445
446
|
columns: table.columns.map((column) => {
|
|
446
447
|
if (!column.maps) return column;
|
|
447
448
|
const maps = fold(aggregate, column.maps);
|
|
@@ -608,6 +609,21 @@ function mergeService(
|
|
|
608
609
|
mine.messages,
|
|
609
610
|
theirs.messages,
|
|
610
611
|
(m) => `${m.direction} ${m.name}`,
|
|
612
|
+
(held, offered) => {
|
|
613
|
+
for (const field of ["encoding", "contentType"] as const) {
|
|
614
|
+
if (!held[field] && offered[field]) {
|
|
615
|
+
held[field] = offered[field];
|
|
616
|
+
} else if (held[field] && offered[field] && held[field] !== offered[field]) {
|
|
617
|
+
conflicts.push({
|
|
618
|
+
path,
|
|
619
|
+
where: incoming.id,
|
|
620
|
+
message: `message "${held.name}" on channel "${mine.address}" of service "${incoming.id}" has ${field} ${offered[field]} here and ${held[field]} in ${owner}; the first one is used`,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return undefined;
|
|
626
|
+
},
|
|
611
627
|
);
|
|
612
628
|
|
|
613
629
|
// Mutated in place: the channel that was here keeps its prose and its
|
|
@@ -620,7 +636,7 @@ function mergeService(
|
|
|
620
636
|
}
|
|
621
637
|
|
|
622
638
|
function copyChannel(channel: Channel): Channel {
|
|
623
|
-
return { ...channel, messages:
|
|
639
|
+
return { ...channel, messages: channel.messages.map((message) => ({ ...message })) };
|
|
624
640
|
}
|
|
625
641
|
|
|
626
642
|
/**
|
|
@@ -26,8 +26,8 @@ const HINTS: { test: RegExp; hint: string }[] = [
|
|
|
26
26
|
hint: "A step names a lane the flow never declared. Add the service to the flow's `participants`, or point the step at one that is already there.",
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
|
-
test: /resolves to neither an Event nor
|
|
30
|
-
hint: "Either the ref is stale — the event was renamed or removed — or the step really does point at something outside the catalog, in which case its status belongs as `unresolved`.",
|
|
29
|
+
test: /resolves to neither an Event, an RpcCall nor a method/,
|
|
30
|
+
hint: "Either the ref is stale — the event or RPC method was renamed or removed — or the step really does point at something outside the catalog, in which case its status belongs as `unresolved`.",
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
test: /must have id/,
|
package/src/pages/Settings.tsx
CHANGED
|
@@ -56,6 +56,7 @@ import { MachineDocs } from "../components/MachineDocs";
|
|
|
56
56
|
import { DeliverySettings } from "./settings/DeliverySettings";
|
|
57
57
|
import { PreferencesSettings } from "./settings/PreferencesSettings";
|
|
58
58
|
import { AboutSettings } from "./settings/AboutSettings";
|
|
59
|
+
import { IntegrationsSettings } from "./settings/IntegrationsSettings";
|
|
59
60
|
import { CatEmptyState, CatIllustration } from "../components/CatIllustration";
|
|
60
61
|
import { CommitLink } from "../components/CommitLink";
|
|
61
62
|
|
|
@@ -954,6 +955,7 @@ const SETTINGS_LINKS = [
|
|
|
954
955
|
["Projects", paths.settingsProjects()],
|
|
955
956
|
["Pipeline", paths.settingsPipeline()],
|
|
956
957
|
["Delivery", paths.settingsDelivery()],
|
|
958
|
+
["Integrations", paths.settingsIntegrations()],
|
|
957
959
|
["Preferences", paths.settingsPreferences()],
|
|
958
960
|
["About", paths.settingsAbout()],
|
|
959
961
|
] as const;
|
|
@@ -1046,6 +1048,10 @@ function OverviewSettings({ local, onGenerate }: { local: boolean; onGenerate: (
|
|
|
1046
1048
|
<div className="font-semibold text-ink">Delivery</div>
|
|
1047
1049
|
<p className="mt-1 text-muted">Install review checks and static catalog publishing for GitHub or GitLab.</p>
|
|
1048
1050
|
</Link>
|
|
1051
|
+
<Link to={paths.settingsIntegrations()} className="card">
|
|
1052
|
+
<div className="font-semibold text-ink">Integrations</div>
|
|
1053
|
+
<p className="mt-1 text-muted">Connect operational tools such as Kafka UI to the catalog.</p>
|
|
1054
|
+
</Link>
|
|
1049
1055
|
<Link to={paths.settingsPreferences()} className="card">
|
|
1050
1056
|
<div className="font-semibold text-ink">Preferences</div>
|
|
1051
1057
|
<p className="mt-1 text-muted">Theme, row density, source editor and Ask the catalog.</p>
|
|
@@ -1112,15 +1118,16 @@ function SettingsContent({ local, onAdd, onRemove, onGenerate }: { local: boolea
|
|
|
1112
1118
|
const pathname = useLocation().pathname.replace(/\/$/, "");
|
|
1113
1119
|
const overview = pathname === paths.settings();
|
|
1114
1120
|
const about = pathname === paths.settingsAbout();
|
|
1121
|
+
const browserIntegration = pathname === paths.settingsIntegrations();
|
|
1115
1122
|
return (
|
|
1116
1123
|
<div className="h-full overflow-y-auto p-gutter">
|
|
1117
1124
|
<div className="max-w-table">
|
|
1118
1125
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
1119
1126
|
<div>
|
|
1120
1127
|
<div className="flex items-center gap-2"><h1 className="text-lg font-semibold">Settings</h1>{local ? <span className="chip status-verified">local mode</span> : null}</div>
|
|
1121
|
-
<p className="mt-1 max-w-prose text-muted">Configure projects, extraction, delivery automation and local preferences. {local ? "This local session can write reviewed changes." : "Build configuration is read-only here."}</p>
|
|
1128
|
+
<p className="mt-1 max-w-prose text-muted">Configure projects, extraction, delivery automation, integrations and local preferences. {local ? "This local session can write reviewed changes." : "Build configuration is read-only here."}</p>
|
|
1122
1129
|
</div>
|
|
1123
|
-
{local && !overview && !about ? <button type="button" className="product-primary" onClick={onGenerate}><Play size={15} /> Preview generated diff</button> : null}
|
|
1130
|
+
{local && !overview && !about && !browserIntegration ? <button type="button" className="product-primary" onClick={onGenerate}><Play size={15} /> Preview generated diff</button> : null}
|
|
1124
1131
|
</div>
|
|
1125
1132
|
<SettingsNav />
|
|
1126
1133
|
<div className="mt-section">
|
|
@@ -1129,12 +1136,13 @@ function SettingsContent({ local, onAdd, onRemove, onGenerate }: { local: boolea
|
|
|
1129
1136
|
<Route path="projects" element={<ProjectsSettings local={local} onAdd={onAdd} onRemove={onRemove} />} />
|
|
1130
1137
|
<Route path="pipeline" element={<PipelineSettings />} />
|
|
1131
1138
|
<Route path="delivery" element={<section><SectionTitle right={local ? "preview before writing" : "local mode required"}>Delivery presets</SectionTitle><DeliverySettings local={local} /></section>} />
|
|
1139
|
+
<Route path="integrations" element={<IntegrationsSettings />} />
|
|
1132
1140
|
<Route path="preferences" element={<PreferencesSettings />} />
|
|
1133
1141
|
<Route path="about" element={<AboutSettings />} />
|
|
1134
1142
|
<Route path="*" element={<Navigate to={paths.settings()} replace />} />
|
|
1135
1143
|
</Routes>
|
|
1136
1144
|
</div>
|
|
1137
|
-
{!about ? <div className="mono mt-section flex items-center gap-2 pb-section text-muted"><Box size={14} aria-hidden />{local ? "Changes are written only after preview; generated files remain reviewable in git." : "Configuration is embedded at build time; changing it requires a new catalog build."}</div> : null}
|
|
1145
|
+
{!about ? <div className="mono mt-section flex items-center gap-2 pb-section text-muted"><Box size={14} aria-hidden />{browserIntegration ? "Integration settings stay in this browser and do not change the generated catalog." : local ? "Changes are written only after preview; generated files remain reviewable in git." : "Configuration is embedded at build time; changing it requires a new catalog build."}</div> : null}
|
|
1138
1146
|
</div>
|
|
1139
1147
|
</div>
|
|
1140
1148
|
);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ExternalLink, Save, Trash2 } from "lucide-react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { SectionTitle } from "../../components/PageHeader";
|
|
4
|
+
import { TechIcon } from "../../components/TechIcon";
|
|
5
|
+
import { normalizeKafkaUiUrl, useKafkaUi } from "../../lib/kafka-ui";
|
|
6
|
+
import { techGlyph } from "../../lib/tech";
|
|
7
|
+
|
|
8
|
+
const FIELD =
|
|
9
|
+
"mono w-full rounded-control border border-line bg-canvas px-3 py-2 text-ink outline-none focus:border-accent";
|
|
10
|
+
|
|
11
|
+
function KafkaUiCard() {
|
|
12
|
+
const configured = useKafkaUi((state) => state.url);
|
|
13
|
+
const setUrl = useKafkaUi((state) => state.setUrl);
|
|
14
|
+
const [draft, setDraft] = useState(configured);
|
|
15
|
+
const normalized = normalizeKafkaUiUrl(draft);
|
|
16
|
+
const valid = normalized !== null;
|
|
17
|
+
const dirty = valid && normalized !== configured;
|
|
18
|
+
const glyph = techGlyph("Kafka");
|
|
19
|
+
|
|
20
|
+
const save = () => {
|
|
21
|
+
if (normalized === null) return;
|
|
22
|
+
setUrl(normalized);
|
|
23
|
+
setDraft(normalized);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const remove = () => {
|
|
27
|
+
setUrl("");
|
|
28
|
+
setDraft("");
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<section className="rounded-card border border-line bg-canvas p-card shadow-xs">
|
|
33
|
+
<div className="flex items-start gap-3">
|
|
34
|
+
<span className="flex size-9 shrink-0 items-center justify-center rounded-control border border-line bg-surface text-ink">
|
|
35
|
+
{glyph ? <TechIcon glyph={glyph} size={18} /> : null}
|
|
36
|
+
</span>
|
|
37
|
+
<div className="min-w-0 flex-1">
|
|
38
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
39
|
+
<h2 className="font-semibold text-ink">Kafka UI</h2>
|
|
40
|
+
<span className={`chip ${configured ? "status-verified" : "text-muted"}`}>
|
|
41
|
+
{configured ? "configured" : "not configured"}
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
<p className="mt-1 text-muted">
|
|
45
|
+
Opens catalogued Kafka topics in your Kafbat Kafka UI installation.
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<label className="mt-4 block">
|
|
51
|
+
<span className="label mb-1.5 block">Kafka UI cluster URL</span>
|
|
52
|
+
<input
|
|
53
|
+
type="url"
|
|
54
|
+
value={draft}
|
|
55
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
56
|
+
onKeyDown={(event) => {
|
|
57
|
+
if (event.key === "Enter" && dirty) save();
|
|
58
|
+
}}
|
|
59
|
+
placeholder="https://kafka.example/ui/clusters/production"
|
|
60
|
+
spellCheck={false}
|
|
61
|
+
autoComplete="off"
|
|
62
|
+
aria-invalid={!valid}
|
|
63
|
+
className={`${FIELD} ${valid ? "" : "border-unresolved"}`}
|
|
64
|
+
/>
|
|
65
|
+
</label>
|
|
66
|
+
<p className={`mono mt-2 ${valid ? "text-muted" : "text-unresolved"}`}>
|
|
67
|
+
{valid
|
|
68
|
+
? "Use a cluster or topics URL to open the exact topic. A plain installation URL opens Kafka UI itself."
|
|
69
|
+
: "Enter an http:// or https:// URL."}
|
|
70
|
+
</p>
|
|
71
|
+
|
|
72
|
+
<div className="mt-4 flex flex-wrap items-center gap-2 border-t border-line pt-4">
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onClick={save}
|
|
76
|
+
disabled={!dirty}
|
|
77
|
+
className="product-primary"
|
|
78
|
+
>
|
|
79
|
+
<Save size={14} aria-hidden />
|
|
80
|
+
Save integration
|
|
81
|
+
</button>
|
|
82
|
+
{configured ? (
|
|
83
|
+
<>
|
|
84
|
+
<a
|
|
85
|
+
href={configured}
|
|
86
|
+
target="_blank"
|
|
87
|
+
rel="noreferrer"
|
|
88
|
+
className="tbtn px-3 py-1.5 text-ink"
|
|
89
|
+
>
|
|
90
|
+
Open Kafka UI
|
|
91
|
+
<ExternalLink size={14} aria-hidden />
|
|
92
|
+
</a>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={remove}
|
|
96
|
+
className="tbtn ml-auto px-3 py-1.5 text-unresolved"
|
|
97
|
+
>
|
|
98
|
+
<Trash2 size={14} aria-hidden />
|
|
99
|
+
Remove
|
|
100
|
+
</button>
|
|
101
|
+
</>
|
|
102
|
+
) : null}
|
|
103
|
+
</div>
|
|
104
|
+
</section>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function IntegrationsSettings() {
|
|
109
|
+
return (
|
|
110
|
+
<section>
|
|
111
|
+
<SectionTitle right="stored in this browser">Integrations</SectionTitle>
|
|
112
|
+
<div className="grid gap-grid lg:grid-cols-2">
|
|
113
|
+
<KafkaUiCard />
|
|
114
|
+
</div>
|
|
115
|
+
</section>
|
|
116
|
+
);
|
|
117
|
+
}
|
package/src/routes.test.ts
CHANGED
|
@@ -169,12 +169,14 @@ describe("routes", () => {
|
|
|
169
169
|
expect(paths.settingsProjects()).toBe("/settings/projects");
|
|
170
170
|
expect(paths.settingsPipeline()).toBe("/settings/pipeline");
|
|
171
171
|
expect(paths.settingsDelivery()).toBe("/settings/delivery");
|
|
172
|
+
expect(paths.settingsIntegrations()).toBe("/settings/integrations");
|
|
172
173
|
expect(paths.settingsPreferences()).toBe("/settings/preferences");
|
|
173
174
|
expect(paths.settingsAbout()).toBe("/settings/about");
|
|
174
175
|
for (const path of [
|
|
175
176
|
paths.settingsProjects(),
|
|
176
177
|
paths.settingsPipeline(),
|
|
177
178
|
paths.settingsDelivery(),
|
|
179
|
+
paths.settingsIntegrations(),
|
|
178
180
|
paths.settingsPreferences(),
|
|
179
181
|
paths.settingsAbout(),
|
|
180
182
|
]) expect(isRoutable(path)).toBe(true);
|
package/src/routes.ts
CHANGED
|
@@ -45,6 +45,7 @@ export const paths = {
|
|
|
45
45
|
settingsProjects: () => "/settings/projects",
|
|
46
46
|
settingsPipeline: () => "/settings/pipeline",
|
|
47
47
|
settingsDelivery: () => "/settings/delivery",
|
|
48
|
+
settingsIntegrations: () => "/settings/integrations",
|
|
48
49
|
settingsPreferences: () => "/settings/preferences",
|
|
49
50
|
settingsAbout: () => "/settings/about",
|
|
50
51
|
external: (slug: string) => `/externals/${slug}`,
|
|
@@ -375,7 +376,7 @@ const ROUTES: RegExp[] = [
|
|
|
375
376
|
/^\/language$/,
|
|
376
377
|
/^\/problems$/,
|
|
377
378
|
/^\/changes$/,
|
|
378
|
-
/^\/settings(?:\/(?:projects|pipeline|delivery|preferences|about))?$/,
|
|
379
|
+
/^\/settings(?:\/(?:projects|pipeline|delivery|integrations|preferences|about))?$/,
|
|
379
380
|
/^\/externals\/[^/]+$/,
|
|
380
381
|
/^\/map$/,
|
|
381
382
|
/^\/adrs\/[^/]+$/,
|
|
@@ -34,7 +34,7 @@ import { stepLabel } from "../flow/labels";
|
|
|
34
34
|
import { KIND_LABEL } from "../lib/kinds";
|
|
35
35
|
import { eventScope, resolveShape } from "../lib/shape";
|
|
36
36
|
import { stepsInto } from "../lib/backlinks";
|
|
37
|
-
import { treeHref } from "../lib/source-link";
|
|
37
|
+
import { sourceLocation, treeHref } from "../lib/source-link";
|
|
38
38
|
import { walkSteps } from "../catalog";
|
|
39
39
|
import { methodCount } from "../lib/api";
|
|
40
40
|
import {
|
|
@@ -49,6 +49,7 @@ import { upstreamOf } from "../er/lineage";
|
|
|
49
49
|
import type { LineageMaps } from "../er/lineage";
|
|
50
50
|
import { ctxStyle } from "../lib/context-color";
|
|
51
51
|
import { Ident } from "../components/Ident";
|
|
52
|
+
import { SourcePreviewLink } from "../components/SourcePreview";
|
|
52
53
|
import { StatusChip } from "../components/primitives";
|
|
53
54
|
import { StepDetailBody } from "../flow/StepDetail";
|
|
54
55
|
import {
|
|
@@ -420,6 +421,50 @@ function TableBody({
|
|
|
420
421
|
</>
|
|
421
422
|
) : null}
|
|
422
423
|
|
|
424
|
+
{(table.accesses ?? []).length > 0 ? (
|
|
425
|
+
<>
|
|
426
|
+
<Label>Reads / writes</Label>
|
|
427
|
+
{(table.accesses ?? []).map((access, accessIndex) => {
|
|
428
|
+
const location = access.source
|
|
429
|
+
? sourceLocation(
|
|
430
|
+
access.source,
|
|
431
|
+
index.serviceById.get(store.owner),
|
|
432
|
+
allRepos(catalog),
|
|
433
|
+
)
|
|
434
|
+
: null;
|
|
435
|
+
const tone =
|
|
436
|
+
access.operation === "read"
|
|
437
|
+
? "text-accent"
|
|
438
|
+
: access.operation === "write"
|
|
439
|
+
? "text-verified"
|
|
440
|
+
: "text-unresolved";
|
|
441
|
+
return (
|
|
442
|
+
<div
|
|
443
|
+
key={`${access.operation}:${access.method}:${access.source}:${accessIndex}`}
|
|
444
|
+
className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-2 gap-y-0.5 py-1"
|
|
445
|
+
>
|
|
446
|
+
<span className={`chip self-start uppercase ${tone}`}>
|
|
447
|
+
{access.operation}
|
|
448
|
+
</span>
|
|
449
|
+
<div className="min-w-0">
|
|
450
|
+
<div className="mono break-all text-ink">
|
|
451
|
+
{access.method ?? "SQL client call"}
|
|
452
|
+
</div>
|
|
453
|
+
{access.source ? (
|
|
454
|
+
<SourcePreviewLink
|
|
455
|
+
location={location}
|
|
456
|
+
className="mono block break-all text-muted hover:text-accent"
|
|
457
|
+
>
|
|
458
|
+
{access.source}
|
|
459
|
+
</SourcePreviewLink>
|
|
460
|
+
) : null}
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
);
|
|
464
|
+
})}
|
|
465
|
+
</>
|
|
466
|
+
) : null}
|
|
467
|
+
|
|
423
468
|
<Label>Referenced by</Label>
|
|
424
469
|
{into.length === 0 ? (
|
|
425
470
|
<div className="mono text-muted">nothing points at this table</div>
|