@shortlink-org/portolan 0.2.1 → 0.2.2
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/cli/init.mjs +14 -6
- package/cli/init.test.mjs +8 -1
- package/cli/portolan.mjs +56 -7
- package/cli/portolan.test.mjs +47 -0
- package/package.json +7 -6
- package/plugins/extract-django/README.md +1 -1
- package/plugins/extract-django/extract_test.py +1 -1
- package/plugins/extract-django/openapi.py +4 -14
- package/plugins/extract-django/openapi_test.py +4 -4
- package/plugins/extract-go/options.schema.json +5 -0
- package/plugins/extract-project/options.schema.json +21 -0
- package/plugins/extract-ts/README.md +1 -1
- package/plugins/extract-ts/clients.ts +1 -1
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/graphql.test.ts +2 -2
- package/plugins/extract-ts/openapi.test.ts +6 -7
- package/plugins/extract-ts/openapi.ts +3 -10
- package/plugins/portolan-go.wasm +0 -0
- package/public/404-cat-v1.webp +0 -0
- package/public/cat-about-v1.webp +0 -0
- package/public/cat-clear-skies-v1.webp +0 -0
- package/public/cat-no-changes-v1.webp +0 -0
- package/public/cat-onboarding-v1.webp +0 -0
- package/public/cat-trial-success-v1.webp +0 -0
- package/schema/portolan.schema.json +61 -0
- package/scripts/catalog-sources.mjs +12 -1
- package/scripts/catalog-sources.test.mjs +35 -0
- package/scripts/gen-likec4.mjs +242 -85
- package/scripts/gen-likec4.test.mjs +168 -14
- package/scripts/gen.mjs +5 -3
- package/scripts/host-plugins/fetch-git.mjs +22 -12
- package/scripts/host-plugins/fetch-git.test.mjs +25 -5
- package/scripts/local-api.mjs +363 -28
- package/scripts/local-api.test.mjs +195 -1
- package/scripts/plugin-host.mjs +14 -3
- package/scripts/plugin-host.test.mjs +8 -0
- package/scripts/schema.mjs +7 -0
- package/src/app/CatalogApp.tsx +19 -4
- package/src/app/CommandPalette.tsx +7 -2
- package/src/app/toast.tsx +11 -7
- package/src/catalog.test.ts +33 -0
- package/src/catalog.ts +190 -8
- package/src/components/CatIllustration.tsx +30 -0
- package/src/components/CommitLink.tsx +154 -0
- package/src/components/EditorLink.tsx +15 -8
- package/src/components/GrpcMethodReference.tsx +93 -0
- package/src/components/Markdown.tsx +17 -0
- package/src/components/MethodRows.tsx +8 -1
- package/src/components/PageHeader.test.ts +24 -0
- package/src/components/PageHeader.tsx +34 -0
- package/src/components/ShapeRows.tsx +12 -0
- package/src/components/SourceDoc.tsx +27 -12
- package/src/components/SourcePreview.tsx +124 -36
- package/src/enrich.test.ts +209 -2
- package/src/enrich.ts +284 -15
- package/src/er/RedisSchema.tsx +86 -5
- package/src/flow/FlowTable.tsx +19 -4
- package/src/flow/StepDetail.tsx +461 -101
- package/src/flow/StepRail.tsx +36 -13
- package/src/flow/answers-response.test.ts +100 -0
- package/src/flow/answers.ts +11 -8
- package/src/flow/chapters.ts +2 -1
- package/src/flow/labels.ts +10 -0
- package/src/flow/mermaid.test.ts +87 -3
- package/src/flow/mermaid.ts +26 -14
- package/src/index.css +132 -0
- package/src/landing/LandingPage.tsx +12 -8
- package/src/lib/api.test.ts +4 -6
- package/src/lib/build-info.test.ts +29 -0
- package/src/lib/build-info.ts +21 -0
- package/src/lib/data-model.ts +14 -0
- package/src/lib/flow-tree.ts +1 -1
- package/src/lib/github-catalog.test.ts +51 -0
- package/src/lib/github-catalog.ts +69 -0
- package/src/lib/grpc-reference.test.ts +72 -0
- package/src/lib/grpc-reference.ts +117 -0
- package/src/lib/local-api.ts +17 -4
- package/src/lib/product.ts +7 -0
- package/src/lib/queries.ts +16 -1
- package/src/lib/readme-assets.test.ts +19 -0
- package/src/lib/readme-assets.ts +34 -0
- package/src/lib/setup-info.test.ts +7 -0
- package/src/lib/setup-info.ts +5 -0
- package/src/lib/source-link.test.ts +106 -18
- package/src/lib/source-link.ts +22 -6
- package/src/merge.test.ts +55 -0
- package/src/merge.ts +30 -0
- package/src/pages/AdrDetail.tsx +3 -2
- package/src/pages/AdrIndex.tsx +10 -7
- package/src/pages/AggregatePage.tsx +34 -4
- package/src/pages/BlockPage.tsx +39 -32
- package/src/pages/Changes.tsx +7 -9
- package/src/pages/ContextMap.tsx +11 -2
- package/src/pages/ContextPage.tsx +11 -4
- package/src/pages/FlowIndex.tsx +16 -7
- package/src/pages/GraphPage.tsx +21 -7
- package/src/pages/Language.tsx +9 -9
- package/src/pages/NotFound.tsx +58 -15
- package/src/pages/Overview.tsx +6 -0
- package/src/pages/Problems.tsx +10 -8
- package/src/pages/RegistryIndex.tsx +10 -8
- package/src/pages/ServicePage.tsx +1 -1
- package/src/pages/Settings.tsx +382 -44
- package/src/routes.test.ts +2 -0
- package/src/routes.ts +2 -1
- package/src/selection/DetailPanel.tsx +213 -113
- package/src/testing/setup.ts +14 -0
package/src/routes.test.ts
CHANGED
|
@@ -170,11 +170,13 @@ describe("routes", () => {
|
|
|
170
170
|
expect(paths.settingsPipeline()).toBe("/settings/pipeline");
|
|
171
171
|
expect(paths.settingsDelivery()).toBe("/settings/delivery");
|
|
172
172
|
expect(paths.settingsPreferences()).toBe("/settings/preferences");
|
|
173
|
+
expect(paths.settingsAbout()).toBe("/settings/about");
|
|
173
174
|
for (const path of [
|
|
174
175
|
paths.settingsProjects(),
|
|
175
176
|
paths.settingsPipeline(),
|
|
176
177
|
paths.settingsDelivery(),
|
|
177
178
|
paths.settingsPreferences(),
|
|
179
|
+
paths.settingsAbout(),
|
|
178
180
|
]) expect(isRoutable(path)).toBe(true);
|
|
179
181
|
expect(isRoutable("/settings/nope")).toBe(false);
|
|
180
182
|
expect(allCatalogPaths(catalog)).toContain(paths.settings());
|
package/src/routes.ts
CHANGED
|
@@ -46,6 +46,7 @@ export const paths = {
|
|
|
46
46
|
settingsPipeline: () => "/settings/pipeline",
|
|
47
47
|
settingsDelivery: () => "/settings/delivery",
|
|
48
48
|
settingsPreferences: () => "/settings/preferences",
|
|
49
|
+
settingsAbout: () => "/settings/about",
|
|
49
50
|
external: (slug: string) => `/externals/${slug}`,
|
|
50
51
|
registry: () => "/registry",
|
|
51
52
|
/**
|
|
@@ -374,7 +375,7 @@ const ROUTES: RegExp[] = [
|
|
|
374
375
|
/^\/language$/,
|
|
375
376
|
/^\/problems$/,
|
|
376
377
|
/^\/changes$/,
|
|
377
|
-
/^\/settings(?:\/(?:projects|pipeline|delivery|preferences))?$/,
|
|
378
|
+
/^\/settings(?:\/(?:projects|pipeline|delivery|preferences|about))?$/,
|
|
378
379
|
/^\/externals\/[^/]+$/,
|
|
379
380
|
/^\/map$/,
|
|
380
381
|
/^\/adrs\/[^/]+$/,
|
|
@@ -19,6 +19,7 @@ import { useNarrow } from "../app/responsive";
|
|
|
19
19
|
import { useUiStore } from "../app/ui-store";
|
|
20
20
|
import { catalog, index } from "../data";
|
|
21
21
|
import {
|
|
22
|
+
allRepos,
|
|
22
23
|
blockFields,
|
|
23
24
|
columnId,
|
|
24
25
|
columnNameOfId,
|
|
@@ -29,9 +30,11 @@ import {
|
|
|
29
30
|
viewReads,
|
|
30
31
|
} from "../catalog";
|
|
31
32
|
import { flowsForService, usesOfDef } from "../lib/derive";
|
|
33
|
+
import { stepLabel } from "../flow/labels";
|
|
32
34
|
import { KIND_LABEL } from "../lib/kinds";
|
|
33
35
|
import { eventScope, resolveShape } from "../lib/shape";
|
|
34
36
|
import { stepsInto } from "../lib/backlinks";
|
|
37
|
+
import { treeHref } from "../lib/source-link";
|
|
35
38
|
import { walkSteps } from "../catalog";
|
|
36
39
|
import { methodCount } from "../lib/api";
|
|
37
40
|
import {
|
|
@@ -78,6 +81,26 @@ function Row({ children }: { children: ReactNode }) {
|
|
|
78
81
|
return <div className="flex items-center gap-2 py-0.5">{children}</div>;
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
function PanelSection({
|
|
85
|
+
title,
|
|
86
|
+
meta,
|
|
87
|
+
children,
|
|
88
|
+
}: {
|
|
89
|
+
title: string;
|
|
90
|
+
meta?: ReactNode;
|
|
91
|
+
children: ReactNode;
|
|
92
|
+
}) {
|
|
93
|
+
return (
|
|
94
|
+
<section className="border-b px-3 py-3 last:border-b-0 border-line">
|
|
95
|
+
<div className="mb-2 flex items-baseline justify-between gap-3">
|
|
96
|
+
<h3 className="label">{title}</h3>
|
|
97
|
+
{meta ? <span className="mono text-muted">{meta}</span> : null}
|
|
98
|
+
</div>
|
|
99
|
+
{children}
|
|
100
|
+
</section>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
81
104
|
/** The panel's own way of moving the selection, without leaving the page. */
|
|
82
105
|
function SelectLink({ id, children }: { id: string; children: ReactNode }) {
|
|
83
106
|
const select = useSelectionStore((s) => s.select);
|
|
@@ -699,6 +722,8 @@ function titleOf(resolved: Resolved | null, selection: Selection): string {
|
|
|
699
722
|
switch (resolved.kind) {
|
|
700
723
|
case "event":
|
|
701
724
|
return resolved.event.name;
|
|
725
|
+
case "service":
|
|
726
|
+
return resolved.service.name;
|
|
702
727
|
case "table":
|
|
703
728
|
return resolved.table.name;
|
|
704
729
|
case "view":
|
|
@@ -706,7 +731,7 @@ function titleOf(resolved: Resolved | null, selection: Selection): string {
|
|
|
706
731
|
case "column":
|
|
707
732
|
return `${resolved.view?.name ?? resolved.table?.name ?? resolved.store.slug}.${resolved.column.name}`;
|
|
708
733
|
case "flow-step":
|
|
709
|
-
return resolved.step
|
|
734
|
+
return stepLabel(resolved.step);
|
|
710
735
|
case "bundle":
|
|
711
736
|
return `${resolved.bundle.from} → ${resolved.bundle.to}`;
|
|
712
737
|
// `acme/shop`, not the registry host as well: the host is the same for
|
|
@@ -842,99 +867,155 @@ function ServiceBody({
|
|
|
842
867
|
const methods = methodCount(service);
|
|
843
868
|
const events = service.aggregates.flatMap((a) => a.events);
|
|
844
869
|
const unresolved = service.consumes.filter((c) => c.status === "unresolved");
|
|
870
|
+
const tree = treeHref(service.path, service, allRepos(catalog));
|
|
845
871
|
// Each flow this service takes part in, opened on the first step it is
|
|
846
872
|
// on either end of - where it enters the story, not the top of the rail.
|
|
847
873
|
const appearances = flowsForService(catalog, service.id).map((flow) => {
|
|
848
874
|
const steps = walkSteps(flow.steps);
|
|
849
|
-
const at = steps.findIndex(
|
|
875
|
+
const at = steps.findIndex(
|
|
876
|
+
(s) => s.from === service.id || s.to === service.id,
|
|
877
|
+
);
|
|
850
878
|
return { flow, step: at >= 0 ? steps[at] : undefined, number: at + 1 };
|
|
851
879
|
});
|
|
852
880
|
|
|
853
881
|
return (
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
882
|
+
<section
|
|
883
|
+
aria-label="Service details"
|
|
884
|
+
className="overflow-hidden rounded-card border shadow-xs border-line"
|
|
885
|
+
>
|
|
886
|
+
<PanelSection title="Source">
|
|
887
|
+
<dl className="grid grid-cols-[6.5rem_minmax(0,1fr)] gap-x-4 gap-y-2">
|
|
888
|
+
<dt className="mono text-muted">Repository</dt>
|
|
889
|
+
<dd className="min-w-0">
|
|
890
|
+
<Ident block value={service.repo} className="text-ink" />
|
|
891
|
+
</dd>
|
|
892
|
+
<dt className="mono text-muted">Path</dt>
|
|
893
|
+
<dd className="min-w-0">
|
|
894
|
+
<Ident block value={service.path} className="text-ink" />
|
|
895
|
+
</dd>
|
|
896
|
+
</dl>
|
|
897
|
+
{tree ? (
|
|
898
|
+
<a
|
|
899
|
+
href={tree}
|
|
900
|
+
target="_blank"
|
|
901
|
+
rel="noreferrer"
|
|
902
|
+
className="mono mt-2 inline-flex items-center rounded-control border px-2 py-1 border-line bg-canvas text-accent hover:bg-raised"
|
|
903
|
+
title="Open the service directory on the forge, at the built commit"
|
|
904
|
+
>
|
|
905
|
+
forge ↗
|
|
906
|
+
</a>
|
|
907
|
+
) : null}
|
|
908
|
+
</PanelSection>
|
|
875
909
|
|
|
876
|
-
<
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
<Row key={call.id}>
|
|
882
|
-
<Ident value={call.id} />
|
|
883
|
-
<span className="ml-auto shrink-0">
|
|
884
|
-
<StatusChip status={call.status} title={call.note} />
|
|
885
|
-
</span>
|
|
886
|
-
</Row>
|
|
887
|
-
))}
|
|
888
|
-
{unresolved.length > 0 ? (
|
|
910
|
+
<PanelSection
|
|
911
|
+
title="Provides"
|
|
912
|
+
meta={`${methods} method${methods === 1 ? "" : "s"}`}
|
|
913
|
+
>
|
|
914
|
+
{/* The count opens the tab that lists the interfaces it counted. */}
|
|
889
915
|
<Link
|
|
890
|
-
to={paths.
|
|
891
|
-
className="mono
|
|
916
|
+
to={`${paths.service(context.id, service.slug)}?tab=provides`}
|
|
917
|
+
className="mono rounded-control text-muted hover:text-ink hover:underline"
|
|
892
918
|
>
|
|
893
|
-
|
|
894
|
-
{
|
|
895
|
-
→
|
|
919
|
+
{service.provides.length} interface
|
|
920
|
+
{service.provides.length === 1 ? "" : "s"} →
|
|
896
921
|
</Link>
|
|
897
|
-
|
|
922
|
+
{service.provides.length > 0 ? (
|
|
923
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
924
|
+
{service.provides.map((provided) => (
|
|
925
|
+
<Ident
|
|
926
|
+
block
|
|
927
|
+
key={provided.id}
|
|
928
|
+
value={provided.id}
|
|
929
|
+
className="text-ink"
|
|
930
|
+
/>
|
|
931
|
+
))}
|
|
932
|
+
</div>
|
|
933
|
+
) : null}
|
|
934
|
+
</PanelSection>
|
|
898
935
|
|
|
899
|
-
<
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
936
|
+
<PanelSection
|
|
937
|
+
title="Consumes"
|
|
938
|
+
meta={`${service.consumes.length} call${service.consumes.length === 1 ? "" : "s"}`}
|
|
939
|
+
>
|
|
940
|
+
{service.consumes.length === 0 ? (
|
|
941
|
+
<div className="mono text-muted">this service calls nobody</div>
|
|
942
|
+
) : (
|
|
943
|
+
<div className="flex flex-col gap-1.5">
|
|
944
|
+
{service.consumes.map((call) => (
|
|
945
|
+
<div key={call.id} className="flex flex-wrap items-center gap-2">
|
|
946
|
+
<Ident value={call.id} />
|
|
947
|
+
<StatusChip status={call.status} title={call.note} />
|
|
948
|
+
</div>
|
|
949
|
+
))}
|
|
950
|
+
</div>
|
|
951
|
+
)}
|
|
952
|
+
{unresolved.length > 0 ? (
|
|
906
953
|
<Link
|
|
907
|
-
to={
|
|
908
|
-
|
|
909
|
-
className="mono tnum ml-auto shrink-0 rounded-control text-muted hover:text-ink"
|
|
954
|
+
to={paths.problems()}
|
|
955
|
+
className="mono mt-2 block rounded-control text-unresolved hover:underline"
|
|
910
956
|
>
|
|
911
|
-
{
|
|
957
|
+
<span className="tnum">{unresolved.length}</span> call
|
|
958
|
+
{unresolved.length === 1 ? "" : "s"} resolve to nothing in the
|
|
959
|
+
catalog →
|
|
912
960
|
</Link>
|
|
913
|
-
|
|
914
|
-
|
|
961
|
+
) : null}
|
|
962
|
+
</PanelSection>
|
|
915
963
|
|
|
916
|
-
<
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
className="
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
964
|
+
<PanelSection
|
|
965
|
+
title="Publishes"
|
|
966
|
+
meta={`${events.length} event${events.length === 1 ? "" : "s"}`}
|
|
967
|
+
>
|
|
968
|
+
{events.length === 0 ? (
|
|
969
|
+
<div className="mono text-muted">no events</div>
|
|
970
|
+
) : (
|
|
971
|
+
<div className="flex flex-col gap-1.5">
|
|
972
|
+
{events.map((event) => (
|
|
973
|
+
<div key={event.id} className="flex flex-wrap items-center gap-2">
|
|
974
|
+
<SelectLink id={event.id}>{event.name}</SelectLink>
|
|
975
|
+
<Link
|
|
976
|
+
to={`${eventPathOf(event.id) ?? paths.service(context.id, service.slug)}#${EVENT_ANCHOR.consumers}`}
|
|
977
|
+
title={`${event.consumers.length} consumers of ${event.name}`}
|
|
978
|
+
className="mono tnum shrink-0 rounded-control text-muted hover:text-ink"
|
|
979
|
+
>
|
|
980
|
+
{event.consumers.length} consumers
|
|
981
|
+
</Link>
|
|
982
|
+
</div>
|
|
983
|
+
))}
|
|
984
|
+
</div>
|
|
985
|
+
)}
|
|
986
|
+
</PanelSection>
|
|
934
987
|
|
|
935
|
-
<
|
|
936
|
-
|
|
937
|
-
|
|
988
|
+
<PanelSection title="Appears in flows" meta={appearances.length}>
|
|
989
|
+
{appearances.length === 0 ? (
|
|
990
|
+
<div className="mono text-muted">appears in no flow</div>
|
|
991
|
+
) : (
|
|
992
|
+
<div className="flex flex-col gap-1.5">
|
|
993
|
+
{appearances.map(({ flow, step, number }) => (
|
|
994
|
+
<Link
|
|
995
|
+
key={flow.slug}
|
|
996
|
+
to={
|
|
997
|
+
step
|
|
998
|
+
? paths.flowStep(flow.slug, step.id)
|
|
999
|
+
: paths.flow(flow.slug)
|
|
1000
|
+
}
|
|
1001
|
+
className="mono flex min-w-0 flex-wrap items-baseline gap-x-2 text-accent"
|
|
1002
|
+
title={step ? `${flow.name}, from step ${number}` : flow.name}
|
|
1003
|
+
>
|
|
1004
|
+
<span className="min-w-0 truncate">{flow.slug}</span>
|
|
1005
|
+
{step ? (
|
|
1006
|
+
<span className="shrink-0 text-muted">step {number}</span>
|
|
1007
|
+
) : null}
|
|
1008
|
+
<span aria-hidden>→</span>
|
|
1009
|
+
</Link>
|
|
1010
|
+
))}
|
|
1011
|
+
</div>
|
|
1012
|
+
)}
|
|
1013
|
+
</PanelSection>
|
|
1014
|
+
|
|
1015
|
+
<PanelSection title="Context">
|
|
1016
|
+
<SelectLink id={context.id}>{context.id}</SelectLink>
|
|
1017
|
+
</PanelSection>
|
|
1018
|
+
</section>
|
|
938
1019
|
);
|
|
939
1020
|
}
|
|
940
1021
|
|
|
@@ -1066,16 +1147,7 @@ function FlowStepBody({
|
|
|
1066
1147
|
}: {
|
|
1067
1148
|
resolved: Extract<Resolved, { kind: "flow-step" }>;
|
|
1068
1149
|
}) {
|
|
1069
|
-
return
|
|
1070
|
-
<>
|
|
1071
|
-
<div className="mono mt-1 mb-2 flex items-center gap-2 text-muted">
|
|
1072
|
-
<Link to={paths.flow(resolved.flow.slug)} className="text-accent">
|
|
1073
|
-
{resolved.flow.slug}
|
|
1074
|
-
</Link>
|
|
1075
|
-
</div>
|
|
1076
|
-
<StepDetailBody step={resolved.step} flow={resolved.flow} />
|
|
1077
|
-
</>
|
|
1078
|
-
);
|
|
1150
|
+
return <StepDetailBody step={resolved.step} flow={resolved.flow} />;
|
|
1079
1151
|
}
|
|
1080
1152
|
|
|
1081
1153
|
/**
|
|
@@ -1208,6 +1280,42 @@ function pinFor(
|
|
|
1208
1280
|
}
|
|
1209
1281
|
}
|
|
1210
1282
|
|
|
1283
|
+
function DetailNavigation({
|
|
1284
|
+
resolved,
|
|
1285
|
+
page,
|
|
1286
|
+
}: {
|
|
1287
|
+
resolved: Resolved | null;
|
|
1288
|
+
page: string | null;
|
|
1289
|
+
}) {
|
|
1290
|
+
if (!page) return null;
|
|
1291
|
+
|
|
1292
|
+
const flowStep = resolved?.kind === "flow-step" ? resolved : null;
|
|
1293
|
+
const destination =
|
|
1294
|
+
flowStep?.flow.slug ??
|
|
1295
|
+
(resolved?.kind === "service" ? "service page" : "catalog page");
|
|
1296
|
+
|
|
1297
|
+
return (
|
|
1298
|
+
<nav
|
|
1299
|
+
aria-label="Selection navigation"
|
|
1300
|
+
className="my-3 flex min-w-0 items-center gap-2 rounded-control border px-2.5 py-2 border-line bg-surface"
|
|
1301
|
+
>
|
|
1302
|
+
<span className="label shrink-0">{flowStep ? "Flow" : "Page"}</span>
|
|
1303
|
+
<span className="mono trunc text-muted" title={destination}>
|
|
1304
|
+
{destination}
|
|
1305
|
+
</span>
|
|
1306
|
+
<Link
|
|
1307
|
+
to={page}
|
|
1308
|
+
className="mono ml-auto inline-flex shrink-0 items-center rounded-control border px-2 py-1 border-line-strong bg-canvas text-accent t-micro transition-colors hover:bg-raised"
|
|
1309
|
+
>
|
|
1310
|
+
{flowStep ? "open flow" : "open"}
|
|
1311
|
+
<span aria-hidden className="ml-1">
|
|
1312
|
+
→
|
|
1313
|
+
</span>
|
|
1314
|
+
</Link>
|
|
1315
|
+
</nav>
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1211
1319
|
export function DetailPanel() {
|
|
1212
1320
|
const selection = useSelectionStore((s) => s.selection);
|
|
1213
1321
|
const clear = useSelectionStore((s) => s.clear);
|
|
@@ -1225,34 +1333,33 @@ export function DetailPanel() {
|
|
|
1225
1333
|
className="panel-in pane flex h-full w-full flex-col border-l border-line bg-canvas"
|
|
1226
1334
|
aria-label="Selection detail"
|
|
1227
1335
|
>
|
|
1228
|
-
<div className="sticky-bar flex shrink-0 items-center gap-2 border-b px-4 py-2
|
|
1336
|
+
<div className="sticky-bar sticky top-0 z-10 flex shrink-0 items-center gap-2 border-b px-4 py-2 border-line">
|
|
1229
1337
|
<span className="label">{kindLabel(selection, resolved)}</span>
|
|
1230
1338
|
{resolved?.kind === "flow-step" ? (
|
|
1231
1339
|
<StatusChip status={resolved.step.status} />
|
|
1232
1340
|
) : null}
|
|
1233
|
-
{/*
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
</button>
|
|
1341
|
+
{/* Panel controls own the far edge even when this selection cannot be
|
|
1342
|
+
pinned. The close button used to sit immediately after the status
|
|
1343
|
+
in that case, which made it look attached to the status itself. */}
|
|
1344
|
+
<div className="ml-auto flex shrink-0 items-center gap-1">
|
|
1345
|
+
{pin ? <PinButton kind={pin.kind} id={pin.id} size={14} /> : null}
|
|
1346
|
+
<button
|
|
1347
|
+
type="button"
|
|
1348
|
+
onClick={() => clear("panel")}
|
|
1349
|
+
aria-label="Close selection detail (Esc)"
|
|
1350
|
+
title="Close · Esc"
|
|
1351
|
+
className="inline-flex size-7 items-center justify-center rounded-control border border-transparent text-muted t-micro transition-colors hover:border-line hover:bg-surface hover:text-ink"
|
|
1352
|
+
>
|
|
1353
|
+
<X size={16} aria-hidden />
|
|
1354
|
+
</button>
|
|
1355
|
+
</div>
|
|
1249
1356
|
</div>
|
|
1250
1357
|
|
|
1251
1358
|
<div className="flex-1 overflow-y-auto p-4">
|
|
1252
1359
|
<div className="flex flex-wrap items-baseline gap-x-2">
|
|
1253
|
-
<
|
|
1360
|
+
<h2 className="mono break-all text-sm font-medium text-ink">
|
|
1254
1361
|
{titleOf(resolved, selection)}
|
|
1255
|
-
</
|
|
1362
|
+
</h2>
|
|
1256
1363
|
{resolved?.kind === "event" ||
|
|
1257
1364
|
resolved?.kind === "service" ||
|
|
1258
1365
|
resolved?.kind === "aggregate" ? (
|
|
@@ -1274,14 +1381,7 @@ export function DetailPanel() {
|
|
|
1274
1381
|
<Ident block value={selection.id} className="mt-0.5 text-muted" />
|
|
1275
1382
|
) : null}
|
|
1276
1383
|
|
|
1277
|
-
{page
|
|
1278
|
-
<Link
|
|
1279
|
-
to={page}
|
|
1280
|
-
className="mono mt-3 inline-block rounded-control text-accent hover:underline"
|
|
1281
|
-
>
|
|
1282
|
-
open page →
|
|
1283
|
-
</Link>
|
|
1284
|
-
) : null}
|
|
1384
|
+
<DetailNavigation resolved={resolved} page={page} />
|
|
1285
1385
|
|
|
1286
1386
|
{resolved === null ? (
|
|
1287
1387
|
<UnknownBody selection={selection} />
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Browser navigation selects a catalog through `?catalog=`. Node has no URL,
|
|
4
|
+
// so tests that exercise the shipped example estate make that same choice by
|
|
5
|
+
// setting the fixture manifest's default profile explicitly.
|
|
6
|
+
vi.mock("../../portolan.json", async (importOriginal) => {
|
|
7
|
+
const module = await importOriginal<{ default: Record<string, unknown> }>();
|
|
8
|
+
return {
|
|
9
|
+
default: {
|
|
10
|
+
...module.default,
|
|
11
|
+
defaultCatalog: "example",
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
});
|