@shortlink-org/portolan 0.2.2 → 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/README.md +7 -2
- package/cli/portolan.mjs +36 -6
- package/package.json +4 -4
- 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/builtin-plugins.mjs +3 -2
- package/scripts/catalog-sources.mjs +2 -1
- package/scripts/catalog-sources.test.mjs +11 -1
- package/scripts/delivery-presets.mjs +207 -24
- package/scripts/diff.mjs +5 -1
- package/scripts/gen-likec4.mjs +78 -20
- package/scripts/gen-likec4.test.mjs +25 -2
- package/scripts/local-api.mjs +26 -377
- package/scripts/local-api.test.mjs +50 -0
- package/scripts/local-discovery.mjs +378 -0
- package/scripts/manifest.mjs +42 -1
- package/scripts/manifest.test.mjs +24 -1
- package/scripts/run-builtin.mjs +4 -2
- package/scripts/schema.mjs +4 -0
- package/scripts/site-docs.mjs +2 -2
- package/src/app/Breadcrumbs.test.ts +4 -0
- package/src/app/Breadcrumbs.tsx +1 -0
- package/src/app/CatalogApp.tsx +4 -4
- package/src/app/Sidebar.tsx +13 -730
- package/src/app/SidebarFlowSections.tsx +251 -0
- package/src/app/SidebarFooter.tsx +160 -0
- package/src/app/SidebarTree.tsx +322 -0
- package/src/catalog-index.ts +485 -0
- package/src/catalog-model.ts +1360 -0
- package/src/catalog-stores.test.ts +17 -0
- package/src/catalog-validation.ts +1611 -0
- package/src/catalog.test.ts +29 -2
- package/src/catalog.ts +6 -3306
- 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/{PageHeader.test.ts → PageHeader.test.tsx} +9 -10
- package/src/components/ProblemRow.tsx +7 -0
- package/src/components/SourcePreview.tsx +1 -1
- 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/index.css +0 -17
- package/src/landing/LandingPage.tsx +4 -4
- package/src/lib/all-problems.ts +1 -1
- 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 +2 -0
- package/src/lib/kafka-ui.test.ts +87 -0
- package/src/lib/kafka-ui.ts +105 -0
- package/src/lib/local-api.ts +19 -7
- package/src/lib/motion.test.ts +4 -2
- package/src/lib/motion.tsx +5 -4
- package/src/lib/proto-problems.test.ts +170 -3
- package/src/lib/proto-problems.ts +176 -4
- 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 +69 -0
- package/src/merge.ts +52 -2
- package/src/pages/CatalogFailure.tsx +2 -2
- package/src/pages/Settings.tsx +12 -129
- package/src/pages/settings/AboutSettings.tsx +129 -0
- package/src/pages/settings/DeliverySettings.tsx +71 -15
- 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/selection/pages.test.ts +14 -1
- package/src/selection/pages.ts +9 -3
- package/vite.config.ts +3 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { Switch } from "@headlessui/react";
|
|
2
3
|
import {
|
|
3
|
-
Check,
|
|
4
4
|
ChevronDown,
|
|
5
5
|
CircleAlert,
|
|
6
6
|
FolderGit2,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
previewDeliveryPreset,
|
|
15
15
|
} from "../../lib/local-api";
|
|
16
16
|
import type {
|
|
17
|
+
DeliveryFeatureId,
|
|
17
18
|
DeliveryPreset,
|
|
18
19
|
DeliveryProvider,
|
|
19
20
|
} from "../../lib/local-api";
|
|
@@ -24,6 +25,7 @@ const STATUS_CLASS: Record<
|
|
|
24
25
|
> = {
|
|
25
26
|
added: "status-verified",
|
|
26
27
|
changed: "status-declared",
|
|
28
|
+
removed: "status-unresolved",
|
|
27
29
|
unchanged: "status-verified",
|
|
28
30
|
conflict: "status-unresolved",
|
|
29
31
|
};
|
|
@@ -35,12 +37,12 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
35
37
|
const [error, setError] = useState("");
|
|
36
38
|
const say = useToastStore((state) => state.say);
|
|
37
39
|
|
|
38
|
-
const load = useCallback(async (provider?: DeliveryProvider) => {
|
|
40
|
+
const load = useCallback(async (provider?: DeliveryProvider, features?: DeliveryFeatureId[]) => {
|
|
39
41
|
if (!local) return;
|
|
40
42
|
setLoading(true);
|
|
41
43
|
setError("");
|
|
42
44
|
try {
|
|
43
|
-
setPlan(await previewDeliveryPreset(provider));
|
|
45
|
+
setPlan(await previewDeliveryPreset(provider, features));
|
|
44
46
|
} catch (cause) {
|
|
45
47
|
setError(cause instanceof Error ? cause.message : String(cause));
|
|
46
48
|
} finally {
|
|
@@ -60,6 +62,7 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
60
62
|
const installed = await installDeliveryPreset(
|
|
61
63
|
plan.provider,
|
|
62
64
|
plan.revision,
|
|
65
|
+
plan.features.filter((feature) => feature.selected).map((feature) => feature.id),
|
|
63
66
|
);
|
|
64
67
|
setPlan(installed);
|
|
65
68
|
say(
|
|
@@ -76,6 +79,23 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
function selectFeature(id: DeliveryFeatureId) {
|
|
83
|
+
if (!plan) return;
|
|
84
|
+
const feature = plan.features.find((item) => item.id === id);
|
|
85
|
+
if (!feature?.available) return;
|
|
86
|
+
const selected = new Set(
|
|
87
|
+
plan.features.filter((item) => item.selected).map((item) => item.id),
|
|
88
|
+
);
|
|
89
|
+
if (selected.has(id)) {
|
|
90
|
+
selected.delete(id);
|
|
91
|
+
if (id === "diff") selected.delete("sarif");
|
|
92
|
+
} else {
|
|
93
|
+
selected.add(id);
|
|
94
|
+
for (const required of feature.requires) selected.add(required);
|
|
95
|
+
}
|
|
96
|
+
void load(plan.provider, [...selected]);
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
if (!local) {
|
|
80
100
|
return (
|
|
81
101
|
<div className="empty">
|
|
@@ -96,8 +116,8 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
96
116
|
<FolderGit2 size={16} className="text-accent" /> Delivery automation
|
|
97
117
|
</div>
|
|
98
118
|
<p className="mt-1 max-w-prose text-muted">
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
Choose the automation this repository needs, preview the generated
|
|
120
|
+
jobs, then install them together.
|
|
101
121
|
</p>
|
|
102
122
|
</div>
|
|
103
123
|
<div
|
|
@@ -111,7 +131,12 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
111
131
|
type="button"
|
|
112
132
|
disabled={loading || installing}
|
|
113
133
|
aria-pressed={plan?.provider === provider}
|
|
114
|
-
onClick={() => void load(
|
|
134
|
+
onClick={() => void load(
|
|
135
|
+
provider,
|
|
136
|
+
plan?.features
|
|
137
|
+
.filter((feature) => feature.selected && (feature.id !== "sarif" || provider === "github"))
|
|
138
|
+
.map((feature) => feature.id),
|
|
139
|
+
)}
|
|
115
140
|
className={`capitalize ${plan?.provider === provider ? "is-on" : ""}`}
|
|
116
141
|
>
|
|
117
142
|
{provider === "github" ? "GitHub" : "GitLab"}
|
|
@@ -157,15 +182,46 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
157
182
|
)}
|
|
158
183
|
</div>
|
|
159
184
|
|
|
160
|
-
<
|
|
185
|
+
<div className="mt-4 grid gap-2 sm:grid-cols-2">
|
|
161
186
|
{plan.features.map((feature) => (
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
187
|
+
<div
|
|
188
|
+
key={feature.id}
|
|
189
|
+
className={`flex items-start gap-3 rounded-control border px-3 py-3 transition-colors ${
|
|
190
|
+
feature.available
|
|
191
|
+
? feature.selected
|
|
192
|
+
? "border-accent bg-surface"
|
|
193
|
+
: "border-line hover:bg-surface"
|
|
194
|
+
: "border-line opacity-60"
|
|
195
|
+
}`}
|
|
196
|
+
>
|
|
197
|
+
<span className="min-w-0 flex-1">
|
|
198
|
+
<span className="block font-medium text-ink">{feature.label}</span>
|
|
199
|
+
<span className="mt-0.5 block text-muted">{feature.description}</span>
|
|
200
|
+
{!feature.available ? (
|
|
201
|
+
<span className="mt-1 block text-faint">Available for GitHub only.</span>
|
|
202
|
+
) : feature.requires.length > 0 ? (
|
|
203
|
+
<span className="mt-1 block text-faint">
|
|
204
|
+
Enables {feature.requires.map((required) => plan.features.find((item) => item.id === required)?.label).join(", ")}.
|
|
205
|
+
</span>
|
|
206
|
+
) : null}
|
|
207
|
+
</span>
|
|
208
|
+
<Switch
|
|
209
|
+
checked={feature.selected}
|
|
210
|
+
disabled={!feature.available || loading || installing}
|
|
211
|
+
onChange={() => selectFeature(feature.id)}
|
|
212
|
+
aria-label={feature.label}
|
|
213
|
+
className="group mt-0.5 inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-line-strong bg-surface transition-colors outline-none data-checked:border-accent data-checked:bg-accent focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-not-allowed"
|
|
214
|
+
>
|
|
215
|
+
<span
|
|
216
|
+
aria-hidden
|
|
217
|
+
className="size-3.5 translate-x-0.5 rounded-full bg-muted shadow-xs transition-transform group-data-checked:translate-x-[18px] group-data-checked:bg-canvas"
|
|
218
|
+
/>
|
|
219
|
+
</Switch>
|
|
220
|
+
</div>
|
|
165
221
|
))}
|
|
166
|
-
</
|
|
222
|
+
</div>
|
|
167
223
|
|
|
168
|
-
<div className="mt-4 divide-y divide-line overflow-hidden rounded-control border border-line">
|
|
224
|
+
{plan.files.length > 0 ? <div className="mt-4 divide-y divide-line overflow-hidden rounded-control border border-line">
|
|
169
225
|
{plan.files.map((file) => (
|
|
170
226
|
<details
|
|
171
227
|
key={file.path}
|
|
@@ -194,7 +250,7 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
194
250
|
</div>
|
|
195
251
|
</details>
|
|
196
252
|
))}
|
|
197
|
-
</div>
|
|
253
|
+
</div> : <div className="empty mt-4">No delivery jobs selected.</div>}
|
|
198
254
|
|
|
199
255
|
{error ? <p className="mt-3 text-unresolved">{error}</p> : null}
|
|
200
256
|
<div className="mt-4 flex flex-wrap items-center justify-between gap-3">
|
|
@@ -204,12 +260,12 @@ export function DeliverySettings({ local }: { local: boolean }) {
|
|
|
204
260
|
</p>
|
|
205
261
|
{plan.status === "installed" ? (
|
|
206
262
|
<span className="flex items-center gap-1.5 text-verified">
|
|
207
|
-
<ShieldCheck size={15} />
|
|
263
|
+
<ShieldCheck size={15} /> Selection installed
|
|
208
264
|
</span>
|
|
209
265
|
) : (
|
|
210
266
|
<button
|
|
211
267
|
type="button"
|
|
212
|
-
className="
|
|
268
|
+
className="product-primary"
|
|
213
269
|
disabled={installing || conflicts.length > 0}
|
|
214
270
|
onClick={() => void install()}
|
|
215
271
|
>
|
|
@@ -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>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { pageContains, selectionPath } from "./pages";
|
|
2
|
+
import { pageContains, selectionPath, selectsInPlace } from "./pages";
|
|
3
3
|
import { selectionFor } from "./model";
|
|
4
4
|
|
|
5
5
|
const dispatched = selectionFor("delivery.core.shipment.ShipmentDispatched");
|
|
@@ -44,6 +44,19 @@ describe("selectionPath", () => {
|
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
describe("selectsInPlace", () => {
|
|
48
|
+
const flow = "/flows/cart-checkout";
|
|
49
|
+
const shop = selectionFor("shop");
|
|
50
|
+
|
|
51
|
+
it("selects a flow context on the flow the first time", () => {
|
|
52
|
+
expect(selectsInPlace(flow, shop, null)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("lets a repeated selection follow its link to the context page", () => {
|
|
56
|
+
expect(selectsInPlace(flow, shop, shop)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
47
60
|
// Navigation keeps a selection only when the page it lands on can point at it.
|
|
48
61
|
// These are the cases that decide whether the palette moves the reader.
|
|
49
62
|
describe("pageContains across pages", () => {
|
package/src/selection/pages.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { walkSteps } from "../catalog";
|
|
|
9
9
|
import { catalog, index } from "../data";
|
|
10
10
|
import { paths } from "../routes";
|
|
11
11
|
import type { Selection } from "./model";
|
|
12
|
-
import { parseFlowStepId, resolveSelection } from "./model";
|
|
12
|
+
import { parseFlowStepId, resolveSelection, sameSelection } from "./model";
|
|
13
13
|
|
|
14
14
|
/** The page that owns a selection, or null when it has no page of its own. */
|
|
15
15
|
export function selectionPath(selection: Selection): string | null {
|
|
@@ -257,12 +257,18 @@ export function pageContains(pathname: string, selection: Selection): boolean {
|
|
|
257
257
|
* and only for something that flow actually draws. That is the one place where
|
|
258
258
|
* leaving is the wrong answer — picking an event there is a question about
|
|
259
259
|
* *this* sequence ("where does it appear?"), and the flow page answers it by
|
|
260
|
-
* lighting the matching steps.
|
|
260
|
+
* lighting the matching steps. Clicking the highlighted selection again is an
|
|
261
|
+
* explicit request to follow the tree link to its own page.
|
|
261
262
|
*/
|
|
262
263
|
export function selectsInPlace(
|
|
263
264
|
pathname: string,
|
|
264
265
|
selection: Selection,
|
|
266
|
+
current: Selection | null,
|
|
265
267
|
): boolean {
|
|
266
268
|
const route = parseRoute(pathname);
|
|
267
|
-
return
|
|
269
|
+
return (
|
|
270
|
+
!sameSelection(current, selection) &&
|
|
271
|
+
route.kind === "flow" &&
|
|
272
|
+
pageContains(pathname, selection)
|
|
273
|
+
);
|
|
268
274
|
}
|
package/vite.config.ts
CHANGED
|
@@ -7,6 +7,8 @@ import react from "@vitejs/plugin-react";
|
|
|
7
7
|
import tailwindcss from "@tailwindcss/vite";
|
|
8
8
|
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
|
9
9
|
import { publicSetupFrom } from "./src/lib/setup-info.ts";
|
|
10
|
+
// @ts-expect-error plain JavaScript module intentionally has no browser types
|
|
11
|
+
import { readManifest } from "./scripts/manifest.mjs";
|
|
10
12
|
// The local control plane is a Node-only Vite plugin kept as plain ESM so its
|
|
11
13
|
// pure discovery functions can also be tested directly.
|
|
12
14
|
// @ts-expect-error plain JavaScript module intentionally has no browser types
|
|
@@ -121,7 +123,7 @@ try {
|
|
|
121
123
|
// A clean checkout has no run to report yet. Settings says so explicitly.
|
|
122
124
|
}
|
|
123
125
|
const setupInfo = publicSetupFrom(
|
|
124
|
-
|
|
126
|
+
readManifest(resolve(workspace, "portolan.json")) as unknown,
|
|
125
127
|
buildReport,
|
|
126
128
|
createHash("sha256").update(manifestText).digest("hex"),
|
|
127
129
|
);
|