@eventcatalog/core 5.7.1 → 5.7.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/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-GRC67WHC.js → chunk-GOIDVIMH.js} +1 -1
- package/dist/{chunk-HZ3XZKUT.js → chunk-L2PFSX4L.js} +1 -1
- package/dist/{chunk-36XVRD36.js → chunk-PLTHZFTW.js} +1 -1
- package/dist/{chunk-CPF5WE3R.js → chunk-PXYVJK54.js} +1 -1
- package/dist/{chunk-AXWTR2FV.js → chunk-RUG7AIT5.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/01-what-is-eventcatalog-assistant.md +8 -0
- package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/02-configuration.md +24 -0
- package/dist/docs/development/studio/01-introduction.md +2 -1
- package/dist/docs/development/studio/04-collaborate-on-a-canvas.md +10 -2
- package/dist/docs/development/studio/05-connect-your-agent.md +4 -0
- package/dist/docs/development/studio/07-reference.md +40 -7
- package/dist/docs/development/studio/draw-with-ai.md +96 -0
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +113 -8
- package/eventcatalog/src/components/ChatPanel/panel-width.ts +28 -0
- package/eventcatalog/src/components/MDX/Steps/Step.astro +4 -2
- package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +2 -103
- package/eventcatalog/src/components/SchemaExplorer/diff-dark-styles.ts +104 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +71 -71
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +1 -1
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +25 -3
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +1 -1
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +1 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_ChangelogPage.astro +297 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.spec.ts +101 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +76 -47
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +4 -168
- package/eventcatalog/src/pages/docs/services/[id]/[version]/changelog/index.astro +14 -0
- package/eventcatalog/src/pages/docs/services/[id]/changelog/index.astro +3 -3
- package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +112 -28
- package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +24 -10
- package/package.json +4 -4
|
@@ -94,6 +94,7 @@ import {
|
|
|
94
94
|
isGroupType,
|
|
95
95
|
type Hotspot,
|
|
96
96
|
type HotspotDraft,
|
|
97
|
+
type HotspotPlace,
|
|
97
98
|
type HotspotFields,
|
|
98
99
|
type HotspotSeverity,
|
|
99
100
|
type HotspotSpot,
|
|
@@ -699,6 +700,8 @@ function Canvas({
|
|
|
699
700
|
const [lastCopied, setLastCopied] = useState<ClipboardContent | null>(null);
|
|
700
701
|
// Nodes to copy instead of the selection (copying from the context menu)
|
|
701
702
|
const copyOnly = useRef<string[] | null>(null);
|
|
703
|
+
// Whether the menu's copy got onto the clipboard (Cut only deletes once it has)
|
|
704
|
+
const copiedFromMenu = useRef(false);
|
|
702
705
|
|
|
703
706
|
/** Adds copies (as one change, so one undo takes them back) and selects them instead of what was selected */
|
|
704
707
|
const paste = useStableCallback((content: ClipboardContent, place: { at: XYPosition } | { offset: XYPosition }) => {
|
|
@@ -731,6 +734,7 @@ function Canvas({
|
|
|
731
734
|
event.clipboardData.setData('text/plain', content.nodes.map((node) => getNodeName(node.type, node.data)).join('\n'));
|
|
732
735
|
pastes.current = 0;
|
|
733
736
|
setLastCopied(content);
|
|
737
|
+
if (fromMenu) copiedFromMenu.current = true;
|
|
734
738
|
if (event.type === 'cut') flow.deleteNodes(ids);
|
|
735
739
|
};
|
|
736
740
|
const onPaste = (event: ClipboardEvent) => {
|
|
@@ -1192,6 +1196,10 @@ function Canvas({
|
|
|
1192
1196
|
setHotspotDraft(null);
|
|
1193
1197
|
});
|
|
1194
1198
|
const cancelHotspot = useCallback(() => setHotspotDraft(null), []);
|
|
1199
|
+
// The hotspot being added, dragged somewhere else before it's added: onto a node, or a place on the level shown
|
|
1200
|
+
const moveHotspotDraft = useStableCallback((to: HotspotPlace) =>
|
|
1201
|
+
setHotspotDraft((draft) => draft && { ...(to.nodeId ? { nodeId: to.nodeId } : placeLevel()), position: to.position })
|
|
1202
|
+
);
|
|
1195
1203
|
const deleteHotspot = useStableCallback((id: string) => flow.deleteHotspot(id));
|
|
1196
1204
|
// Dragged onto a node (they move with it), or to a place; open where it lands if it was open
|
|
1197
1205
|
const moveHotspots = useStableCallback((ids: string[], to: { nodeId?: string; position: XYPosition }) => {
|
|
@@ -1213,8 +1221,8 @@ function Canvas({
|
|
|
1213
1221
|
const width = node ? (node.measured?.width ?? node.width ?? 0) : 0;
|
|
1214
1222
|
void setCenter(at.x + width / 2 + 120, at.y + 80, { zoom: Math.max(getZoom(), 0.8), duration: 300 });
|
|
1215
1223
|
setHotspotDraft(null);
|
|
1224
|
+
// (the list stays open, so they can go through them one by one)
|
|
1216
1225
|
setOpenHotspot(node ? `node:${node.id}` : `place:${hotspot.id}`);
|
|
1217
|
-
setListingHotspots(false);
|
|
1218
1226
|
});
|
|
1219
1227
|
const nameOfNode = useStableCallback((id: string) => {
|
|
1220
1228
|
const node = nodesRef.current.find((candidate) => candidate.id === id);
|
|
@@ -1725,6 +1733,13 @@ function Canvas({
|
|
|
1725
1733
|
flow.deleteNodes(menuTargets());
|
|
1726
1734
|
setContextMenu(null);
|
|
1727
1735
|
});
|
|
1736
|
+
// Copies, then deletes, as ⌘X does: only on L3, where copying works (so it's not offered on L1 and L2)
|
|
1737
|
+
const cutFromMenu = useStableCallback(() => {
|
|
1738
|
+
copiedFromMenu.current = false;
|
|
1739
|
+
copyFromMenu();
|
|
1740
|
+
// Not copied (the browser refused, or there was nothing to copy): nothing's deleted, as with ⌘X
|
|
1741
|
+
if (copiedFromMenu.current) deleteFromMenu();
|
|
1742
|
+
});
|
|
1728
1743
|
|
|
1729
1744
|
const openThread = useStableCallback((thread: Thread) => {
|
|
1730
1745
|
setListingComments(false);
|
|
@@ -1841,7 +1856,7 @@ function Canvas({
|
|
|
1841
1856
|
ref={canvasRef}
|
|
1842
1857
|
className={`eventcatalog-visualizer studio-canvas relative flex-1 min-w-0 ${commentMode ? 'comment-mode' : ''} ${
|
|
1843
1858
|
transitioning ? 'is-layout-transition' : ''
|
|
1844
|
-
} ${placing || tool === 'comment' || tool === 'hotspot' ? 'is-placing' : ''} ${tool === 'hand' ? 'is-panning' : ''}`}
|
|
1859
|
+
} ${placing || tool === 'comment' || tool === 'hotspot' ? 'is-placing' : ''} ${tool === 'hotspot' ? 'is-placing-hotspot' : ''} ${tool === 'hand' ? 'is-panning' : ''}`}
|
|
1845
1860
|
onPointerMove={trackPointer}
|
|
1846
1861
|
onPointerLeave={clearPointer}
|
|
1847
1862
|
onDoubleClick={onCanvasDoubleClick}
|
|
@@ -1938,6 +1953,7 @@ function Canvas({
|
|
|
1938
1953
|
onUpdate={flow.updateHotspot}
|
|
1939
1954
|
onDelete={deleteHotspot}
|
|
1940
1955
|
onGo={goToHotspot}
|
|
1956
|
+
onMoveDraft={moveHotspotDraft}
|
|
1941
1957
|
/>
|
|
1942
1958
|
</ReactFlow>
|
|
1943
1959
|
|
|
@@ -2051,7 +2067,12 @@ function Canvas({
|
|
|
2051
2067
|
</button>
|
|
2052
2068
|
</div>
|
|
2053
2069
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
2054
|
-
<HotspotList
|
|
2070
|
+
<HotspotList
|
|
2071
|
+
hotspots={flow.hotspots}
|
|
2072
|
+
nameOf={nameOfNode}
|
|
2073
|
+
onOpen={goToHotspot}
|
|
2074
|
+
goOnHover={!hotspotDraft}
|
|
2075
|
+
/>
|
|
2055
2076
|
</div>
|
|
2056
2077
|
</div>
|
|
2057
2078
|
)}
|
|
@@ -2081,6 +2102,7 @@ function Canvas({
|
|
|
2081
2102
|
onAddNote={noteFromMenu}
|
|
2082
2103
|
onEditDetails={editFromMenu}
|
|
2083
2104
|
onCopy={copyFromMenu}
|
|
2105
|
+
onCut={level === 3 ? cutFromMenu : undefined}
|
|
2084
2106
|
onDuplicate={duplicateFromMenu}
|
|
2085
2107
|
onPaste={lastCopied ? pasteFromMenu : undefined}
|
|
2086
2108
|
onReorder={reorderFromMenu}
|
|
@@ -277,7 +277,7 @@ const createCanvasTools = (get: () => CanvasWebMcpOptions, getStage: () => Agent
|
|
|
277
277
|
defineTool({
|
|
278
278
|
name: 'remove_from_canvas',
|
|
279
279
|
title: 'Remove from the canvas',
|
|
280
|
-
description: 'Removes nodes (and their connections) or connections. Only remove what the user asked for.',
|
|
280
|
+
description: 'Removes nodes (and their connections and hotspots) or connections. Only remove what the user asked for.',
|
|
281
281
|
input: z.object({ nodeIds: z.array(z.string()).optional(), connectionIds: z.array(z.string()).optional() }),
|
|
282
282
|
annotations: { consequentialHint: true },
|
|
283
283
|
execute: async ({ nodeIds = [], connectionIds = [] }) => {
|
|
@@ -372,7 +372,7 @@ export function registerCanvasTools(server: McpServer, { catalogUrl, userAgent }
|
|
|
372
372
|
{
|
|
373
373
|
title: 'Remove from a canvas',
|
|
374
374
|
description:
|
|
375
|
-
'Removes nodes (and their connections) or connections from a canvas. Comments on removed nodes stay on the canvas. Only remove what the user asked for or what you added.',
|
|
375
|
+
'Removes nodes (and their connections and hotspots) or connections from a canvas. Comments on removed nodes stay on the canvas. Only remove what the user asked for or what you added.',
|
|
376
376
|
inputSchema: z.object({
|
|
377
377
|
canvasId,
|
|
378
378
|
nodeIds: z.array(z.string()).optional(),
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Footer from '@layouts/Footer.astro';
|
|
3
|
+
import { getChangeLogs } from '@utils/collections/changelogs';
|
|
4
|
+
import { render, getEntry } from 'astro:content';
|
|
5
|
+
import mdxComponents from '@components/MDX/components';
|
|
6
|
+
import 'diff2html/bundles/css/diff2html.min.css';
|
|
7
|
+
import { getDiffDarkStyles } from '@components/SchemaExplorer/diff-dark-styles';
|
|
8
|
+
|
|
9
|
+
import { buildUrl } from '@utils/url-builder';
|
|
10
|
+
import { getPreviousVersion, getResourceNoun } from '@utils/collections/util';
|
|
11
|
+
import { getDiffsForCurrentAndPreviousVersion } from '@utils/file-diffs';
|
|
12
|
+
import { isIconPath, resolveIconUrl } from '@utils/icon';
|
|
13
|
+
import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
|
|
14
|
+
import Badge from '@components/Badge.astro';
|
|
15
|
+
import { ClientRouter } from 'astro:transitions';
|
|
16
|
+
import { HistoryIcon } from 'lucide-react';
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
changelogOwner: any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = Astro.props.changelogOwner;
|
|
23
|
+
|
|
24
|
+
let collectionItem = props;
|
|
25
|
+
const logs = await getChangeLogs(props);
|
|
26
|
+
|
|
27
|
+
const { data } = props;
|
|
28
|
+
const latestVersion = data.latestVersion;
|
|
29
|
+
|
|
30
|
+
const renderedLogs = logs.map(async (log) => {
|
|
31
|
+
const logEntry = await getEntry('changelogs', log.id);
|
|
32
|
+
const { Content } = await render(logEntry as any);
|
|
33
|
+
return {
|
|
34
|
+
Content,
|
|
35
|
+
...log,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const logsToRender = await Promise.all(renderedLogs);
|
|
40
|
+
|
|
41
|
+
const logListPromise = logsToRender.map(async (log) => {
|
|
42
|
+
const currentLogVersion = log.data.version;
|
|
43
|
+
const previousLogVersion = log.data.version ? getPreviousVersion(log.data.version, data.versions || []) : '';
|
|
44
|
+
return {
|
|
45
|
+
id: log.id,
|
|
46
|
+
// Anchor for the version, linked from the "Versions" list on the right
|
|
47
|
+
anchor: `v${log.data.version}`,
|
|
48
|
+
url: buildUrl(`/docs/${props.collection}/${props.data.id}/${log.data.version}`),
|
|
49
|
+
isLatest: log.data.version === latestVersion,
|
|
50
|
+
version: log.data.version,
|
|
51
|
+
previousVersion: previousLogVersion,
|
|
52
|
+
createdAt: log.data.createdAt,
|
|
53
|
+
badges: log.data.badges || [],
|
|
54
|
+
Content: log.Content,
|
|
55
|
+
diffs:
|
|
56
|
+
currentLogVersion && previousLogVersion
|
|
57
|
+
? await getDiffsForCurrentAndPreviousVersion(
|
|
58
|
+
currentLogVersion,
|
|
59
|
+
previousLogVersion,
|
|
60
|
+
collectionItem.data.id,
|
|
61
|
+
props.allCollectionItems
|
|
62
|
+
)
|
|
63
|
+
: [],
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const logList = await Promise.all(logListPromise);
|
|
68
|
+
|
|
69
|
+
const friendlyCollectionName = getResourceNoun(props.collection);
|
|
70
|
+
const headerIconSrc = isIconPath(props.data.styles?.icon) ? resolveIconUrl(props.data.styles!.icon!) : null;
|
|
71
|
+
|
|
72
|
+
const formatDate = (date?: Date) =>
|
|
73
|
+
date?.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' });
|
|
74
|
+
|
|
75
|
+
const releaseCount = `${logList.length} ${logList.length === 1 ? 'release' : 'releases'}`;
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
<VerticalSideBarLayout title={`${props.data.name} | Changelog`} description={props.data.summary}>
|
|
79
|
+
<main class="flex docs-layout min-h-full bg-[rgb(var(--ec-page-bg))]">
|
|
80
|
+
<div class="flex docs-layout w-full">
|
|
81
|
+
<div class="w-full min-w-0 lg:mr-2 xl:pr-12 py-8 bg-[rgb(var(--ec-page-bg))]">
|
|
82
|
+
<div class="border-b border-[rgb(var(--ec-page-border))] md:pb-3 pt-4">
|
|
83
|
+
<div class="flex flex-col gap-4">
|
|
84
|
+
<span class="text-xs md:text-sm font-semibold text-[rgb(var(--ec-accent))]">Changelog</span>
|
|
85
|
+
<div class="flex items-center gap-3">
|
|
86
|
+
{
|
|
87
|
+
headerIconSrc && (
|
|
88
|
+
<img src={headerIconSrc} alt={props.data.name} class="w-8 h-8 md:w-10 md:h-10 object-contain rounded-md shrink-0" />
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
<h2 id="doc-page-header" class="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">
|
|
92
|
+
{props.data.name}
|
|
93
|
+
</h2>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<p class="text-base pt-4 text-[rgb(var(--ec-page-text-muted))] font-light">
|
|
97
|
+
What changed in each version of {props.data.name}, newest first.
|
|
98
|
+
</p>
|
|
99
|
+
<div class="flex flex-wrap items-center gap-x-3 gap-y-2 pt-5 text-xs text-[rgb(var(--ec-page-text-muted))]">
|
|
100
|
+
<span>{releaseCount}</span>
|
|
101
|
+
{
|
|
102
|
+
latestVersion && (
|
|
103
|
+
<>
|
|
104
|
+
<span aria-hidden="true" class="h-3 w-px bg-[rgb(var(--ec-page-border))]" />
|
|
105
|
+
<span>Latest v{latestVersion}</span>
|
|
106
|
+
</>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
logList.length === 0 && (
|
|
114
|
+
<div class="mt-8 rounded-lg border border-dashed border-[rgb(var(--ec-page-border))] px-6 py-10 text-center">
|
|
115
|
+
<HistoryIcon className="mx-auto h-6 w-6 text-[rgb(var(--ec-icon-color))]" strokeWidth={1.5} />
|
|
116
|
+
<p class="mt-3 text-sm font-semibold text-[rgb(var(--ec-page-text))]">No changelogs yet</p>
|
|
117
|
+
<p class="mt-1 text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
118
|
+
Add a <code>changelog.mdx</code> file next to this {friendlyCollectionName} to record what changed.
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
<ol class="changelog-timeline pt-4">
|
|
125
|
+
{
|
|
126
|
+
logList.map((log, index) => (
|
|
127
|
+
<li id={log.anchor} class="changelog-release relative scroll-mt-20 pl-8 pt-8">
|
|
128
|
+
{index !== logList.length - 1 && (
|
|
129
|
+
<span aria-hidden="true" class="absolute left-[5px] top-[2.9rem] -bottom-[2.6rem] w-px bg-[rgb(var(--ec-page-border))]" />
|
|
130
|
+
)}
|
|
131
|
+
<span
|
|
132
|
+
aria-hidden="true"
|
|
133
|
+
class={`absolute left-0 top-[2.6rem] h-[11px] w-[11px] rounded-full ring-4 ring-[rgb(var(--ec-page-bg))] ${
|
|
134
|
+
log.isLatest ? 'bg-[rgb(var(--ec-accent))]' : 'bg-[rgb(var(--ec-page-border))]'
|
|
135
|
+
}`}
|
|
136
|
+
/>
|
|
137
|
+
<div class="flex flex-wrap items-center gap-x-3 gap-y-2">
|
|
138
|
+
<a
|
|
139
|
+
href={log.url}
|
|
140
|
+
class="text-xl font-bold text-[rgb(var(--ec-page-text))] hover:text-[rgb(var(--ec-accent))] transition-colors"
|
|
141
|
+
>
|
|
142
|
+
v{log.version}
|
|
143
|
+
</a>
|
|
144
|
+
{log.isLatest && (
|
|
145
|
+
<span class="rounded-full bg-[rgb(var(--ec-accent-subtle))] px-2 py-0.5 text-xs font-medium text-[rgb(var(--ec-accent))]">
|
|
146
|
+
Latest
|
|
147
|
+
</span>
|
|
148
|
+
)}
|
|
149
|
+
{log.createdAt && (
|
|
150
|
+
<time datetime={log.createdAt.toISOString()} class="text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
151
|
+
{formatDate(log.createdAt)}
|
|
152
|
+
</time>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
{log.badges.filter(Boolean).length > 0 && (
|
|
156
|
+
<div class="flex flex-wrap gap-1.5 pt-3">
|
|
157
|
+
{log.badges.filter(Boolean).map((badge: any) => (
|
|
158
|
+
<Badge badge={badge} />
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
<div class="prose prose-md pt-2 pb-4 w-full text-[15px]">
|
|
163
|
+
<log.Content components={mdxComponents(props)} />
|
|
164
|
+
</div>
|
|
165
|
+
{log.diffs && log.diffs.length > 0 && (
|
|
166
|
+
<details class="changelog-diffs group mb-4 rounded-lg border border-[rgb(var(--ec-page-border))] overflow-hidden">
|
|
167
|
+
<summary class="flex cursor-pointer list-none items-center justify-between gap-3 bg-[rgb(var(--ec-content-hover))] px-4 py-2.5 text-sm">
|
|
168
|
+
<span class="text-[rgb(var(--ec-page-text))]">
|
|
169
|
+
<span class="font-semibold">v{log.previousVersion}</span>
|
|
170
|
+
<span class="mx-2 text-[rgb(var(--ec-page-text-muted))]">→</span>
|
|
171
|
+
<span class="font-semibold">v{log.version}</span>
|
|
172
|
+
</span>
|
|
173
|
+
<span class="text-xs text-[rgb(var(--ec-page-text-muted))]">
|
|
174
|
+
{log.diffs.length} {log.diffs.length === 1 ? 'file changed' : 'files changed'}
|
|
175
|
+
<span class="ml-1 inline-block transition-transform group-open:rotate-90">›</span>
|
|
176
|
+
</span>
|
|
177
|
+
</summary>
|
|
178
|
+
<div class="space-y-4 border-t border-[rgb(var(--ec-page-border))] p-2">
|
|
179
|
+
{log.diffs.map((diff) => (
|
|
180
|
+
<div set:html={diff} />
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
</details>
|
|
184
|
+
)}
|
|
185
|
+
</li>
|
|
186
|
+
))
|
|
187
|
+
}
|
|
188
|
+
</ol>
|
|
189
|
+
<Footer />
|
|
190
|
+
</div>
|
|
191
|
+
{
|
|
192
|
+
logList.length > 0 && (
|
|
193
|
+
<aside class="hidden xl:block sticky top-[4rem] self-start w-[280px] max-h-[calc(100vh-4rem)] overflow-y-auto py-2 flex-shrink-0 pr-4 bg-[rgb(var(--ec-page-bg))]">
|
|
194
|
+
<div class="mt-8">
|
|
195
|
+
<h3 class="text-xs text-[rgb(var(--ec-page-text))] font-semibold capitalize flex items-center gap-2 mb-4">
|
|
196
|
+
<HistoryIcon className="w-4 h-4" />
|
|
197
|
+
Versions
|
|
198
|
+
</h3>
|
|
199
|
+
<nav id="changelog-versions" class="text-xs border-l border-[rgb(var(--ec-page-border))]">
|
|
200
|
+
{logList.map((log) => (
|
|
201
|
+
<a
|
|
202
|
+
href={`#${log.anchor}`}
|
|
203
|
+
data-version-anchor={log.anchor}
|
|
204
|
+
class="flex items-center justify-between gap-2 py-1.5 pl-3 pr-2.5 leading-5 text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] border-l-2 border-transparent -ml-px transition-all duration-200"
|
|
205
|
+
>
|
|
206
|
+
<span>
|
|
207
|
+
v{log.version}
|
|
208
|
+
{log.isLatest && <span class="ml-1 text-[rgb(var(--ec-page-text-muted))]">(latest)</span>}
|
|
209
|
+
</span>
|
|
210
|
+
{log.createdAt && (
|
|
211
|
+
<span class="shrink-0 text-[11px] text-[rgb(var(--ec-page-text-muted))]">
|
|
212
|
+
{log.createdAt.toISOString().split('T')[0]}
|
|
213
|
+
</span>
|
|
214
|
+
)}
|
|
215
|
+
</a>
|
|
216
|
+
))}
|
|
217
|
+
</nav>
|
|
218
|
+
</div>
|
|
219
|
+
</aside>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<style is:global set:html={getDiffDarkStyles(":root[data-theme='dark'] .changelog-diffs")} />
|
|
225
|
+
<style is:global>
|
|
226
|
+
.docs-layout .prose {
|
|
227
|
+
max-width: none;
|
|
228
|
+
overflow: auto;
|
|
229
|
+
}
|
|
230
|
+
.changelog-diffs summary::-webkit-details-marker {
|
|
231
|
+
display: none;
|
|
232
|
+
}
|
|
233
|
+
#changelog-versions a.is-active {
|
|
234
|
+
color: rgb(var(--ec-accent));
|
|
235
|
+
border-color: rgb(var(--ec-accent));
|
|
236
|
+
font-weight: 500;
|
|
237
|
+
}
|
|
238
|
+
</style>
|
|
239
|
+
|
|
240
|
+
<script>
|
|
241
|
+
declare global {
|
|
242
|
+
interface Window {
|
|
243
|
+
__changelogVersionsScroll?: (event: Event) => void;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Highlight the version in view in the "Versions" list, like "On this page" does for headings.
|
|
248
|
+
function setupVersionList() {
|
|
249
|
+
if (window.__changelogVersionsScroll) {
|
|
250
|
+
document.removeEventListener('scroll', window.__changelogVersionsScroll, true);
|
|
251
|
+
window.__changelogVersionsScroll = undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const nav = document.getElementById('changelog-versions');
|
|
255
|
+
if (!nav) return;
|
|
256
|
+
|
|
257
|
+
const links = Array.from(nav.querySelectorAll<HTMLAnchorElement>('a[data-version-anchor]'));
|
|
258
|
+
const setActive = (anchor: string) => {
|
|
259
|
+
links.forEach((link) => link.classList.toggle('is-active', link.dataset.versionAnchor === anchor));
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
let paused = false;
|
|
263
|
+
links.forEach((link) =>
|
|
264
|
+
link.addEventListener('click', () => {
|
|
265
|
+
setActive(link.dataset.versionAnchor!);
|
|
266
|
+
paused = true;
|
|
267
|
+
setTimeout(() => (paused = false), 800);
|
|
268
|
+
})
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Each release is one tall block, so pick the last one whose top has scrolled past the
|
|
272
|
+
// top third of the screen, or the last release once the page is scrolled to the bottom.
|
|
273
|
+
const releases = Array.from(document.querySelectorAll<HTMLElement>('.changelog-release'));
|
|
274
|
+
const onScroll = (event: Event) => {
|
|
275
|
+
// Ignore other scrolling areas, like the sidebar
|
|
276
|
+
if (paused || (event.target instanceof HTMLElement && !event.target.contains(releases[0]))) return;
|
|
277
|
+
const scroller =
|
|
278
|
+
event.target instanceof HTMLElement ? event.target : (document.scrollingElement as HTMLElement | null);
|
|
279
|
+
const atBottom = scroller && scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 4;
|
|
280
|
+
const current = atBottom
|
|
281
|
+
? releases[releases.length - 1]
|
|
282
|
+
: releases.filter((release) => release.getBoundingClientRect().top <= window.innerHeight / 3).pop();
|
|
283
|
+
setActive((current ?? releases[0])?.id ?? '');
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
window.__changelogVersionsScroll = onScroll;
|
|
287
|
+
document.addEventListener('scroll', onScroll, { capture: true, passive: true });
|
|
288
|
+
|
|
289
|
+
setActive(decodeURIComponent(location.hash.slice(1)) || links[0]?.dataset.versionAnchor || '');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// The page uses the ClientRouter, so this fires on the first load and after every navigation.
|
|
293
|
+
document.addEventListener('astro:page-load', setupVersionList);
|
|
294
|
+
</script>
|
|
295
|
+
</main>
|
|
296
|
+
<ClientRouter />
|
|
297
|
+
</VerticalSideBarLayout>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getLatestServiceChangelogPaths, Page, ServicePage } from './_index.data';
|
|
3
|
+
|
|
4
|
+
const COLLECTIONS = ['agents', 'events', 'commands', 'queries', 'services', 'domains', 'systems', 'flows', 'containers'];
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({
|
|
7
|
+
isSSR: vi.fn(),
|
|
8
|
+
isChangelogEnabled: vi.fn(),
|
|
9
|
+
loaders: {} as Record<string, ReturnType<typeof vi.fn>>,
|
|
10
|
+
getServices: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock('@utils/feature', () => ({
|
|
14
|
+
isSSR: () => mocks.isSSR(),
|
|
15
|
+
isChangelogEnabled: () => mocks.isChangelogEnabled(),
|
|
16
|
+
}));
|
|
17
|
+
vi.mock('@utils/changelog-resource-badge', () => ({
|
|
18
|
+
changelogResourceCollections: ['agents', 'events', 'commands', 'queries', 'services', 'domains', 'systems', 'flows', 'containers'],
|
|
19
|
+
}));
|
|
20
|
+
vi.mock('@utils/collections/services', () => ({
|
|
21
|
+
getServices: () => mocks.getServices(),
|
|
22
|
+
}));
|
|
23
|
+
vi.mock('@utils/page-loaders/page-data-loader', () => ({
|
|
24
|
+
pageDataLoader: new Proxy({}, { get: (_target, type: string) => mocks.loaders[type] }),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const item = (id: string, version: string) => ({ collection: 'any', data: { id, version } });
|
|
28
|
+
|
|
29
|
+
describe('Changelog routes', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
mocks.isSSR.mockReset().mockReturnValue(false);
|
|
32
|
+
mocks.isChangelogEnabled.mockReset().mockReturnValue(true);
|
|
33
|
+
mocks.loaders = Object.fromEntries(COLLECTIONS.map((type) => [type, vi.fn().mockResolvedValue([item(`${type}-1`, '1.0.0')])]));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('gives every type but services a path on the generic route', async () => {
|
|
37
|
+
const paths = await Page.getStaticPaths();
|
|
38
|
+
|
|
39
|
+
expect(paths.map(({ params }) => params.type)).toEqual(COLLECTIONS.filter((type) => type !== 'services'));
|
|
40
|
+
expect(paths[0].params).toEqual({ type: 'agents', id: 'agents-1', version: '1.0.0' });
|
|
41
|
+
expect(mocks.loaders.services).not.toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('gives one path per service id and version on the service route', async () => {
|
|
45
|
+
const orders1 = item('Orders', '1.0.0');
|
|
46
|
+
mocks.loaders.services.mockResolvedValue([orders1, item('Orders', '2.0.0'), item('Billing', '1.0.0')]);
|
|
47
|
+
|
|
48
|
+
const paths = await ServicePage.getStaticPaths();
|
|
49
|
+
|
|
50
|
+
expect(paths.map(({ params }) => params)).toEqual([
|
|
51
|
+
{ id: 'Orders', version: '1.0.0' },
|
|
52
|
+
{ id: 'Orders', version: '2.0.0' },
|
|
53
|
+
{ id: 'Billing', version: '1.0.0' },
|
|
54
|
+
]);
|
|
55
|
+
expect(paths[0].props).toMatchObject({ type: 'services', ...orders1 });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('has no static paths on a server', async () => {
|
|
59
|
+
mocks.isSSR.mockReturnValue(true);
|
|
60
|
+
|
|
61
|
+
expect(await Page.getStaticPaths()).toEqual([]);
|
|
62
|
+
expect(await ServicePage.getStaticPaths()).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('has no static paths when the changelog is disabled', async () => {
|
|
66
|
+
mocks.isChangelogEnabled.mockReturnValue(false);
|
|
67
|
+
|
|
68
|
+
expect(await Page.getStaticPaths()).toEqual([]);
|
|
69
|
+
expect(await ServicePage.getStaticPaths()).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('forces the services type when fetching on the service route', async () => {
|
|
73
|
+
mocks.loaders.services.mockResolvedValue([item('Orders', '1.0.0')]);
|
|
74
|
+
|
|
75
|
+
const data = await ServicePage.getData({ params: { id: 'Orders', version: '1.0.0', type: 'events' }, props: {} } as any);
|
|
76
|
+
|
|
77
|
+
expect(data).toMatchObject({ type: 'services', data: { id: 'Orders', version: '1.0.0' } });
|
|
78
|
+
expect(mocks.loaders.events).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns nothing for an unknown version or a disabled changelog', async () => {
|
|
82
|
+
mocks.loaders.services.mockResolvedValue([item('Orders', '1.0.0')]);
|
|
83
|
+
|
|
84
|
+
await expect(ServicePage.getData({ params: { id: 'Orders', version: '9.9.9' }, props: {} } as any)).resolves.toBeUndefined();
|
|
85
|
+
|
|
86
|
+
mocks.isChangelogEnabled.mockReturnValue(false);
|
|
87
|
+
await expect(ServicePage.getData({ params: { id: 'Orders', version: '1.0.0' }, props: {} } as any)).resolves.toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('gives the version-less service changelog alias a path for the latest version of each service', async () => {
|
|
91
|
+
mocks.loaders.services.mockResolvedValue([item('Orders', '1.0.0'), item('Orders', '2.0.0'), item('Billing', '1.0.0')]);
|
|
92
|
+
mocks.getServices.mockResolvedValue([item('Orders', '2.0.0'), item('Billing', '1.0.0')]);
|
|
93
|
+
|
|
94
|
+
const paths = await getLatestServiceChangelogPaths();
|
|
95
|
+
|
|
96
|
+
expect(paths.map(({ params, props }) => ({ params, redirectVersion: props?.redirectVersion }))).toEqual([
|
|
97
|
+
{ params: { id: 'Orders' }, redirectVersion: '2.0.0' },
|
|
98
|
+
{ params: { id: 'Billing' }, redirectVersion: '1.0.0' },
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -2,61 +2,90 @@ import { isSSR, isChangelogEnabled } from '@utils/feature';
|
|
|
2
2
|
import { changelogResourceCollections } from '@utils/changelog-resource-badge';
|
|
3
3
|
import { HybridPage } from '@utils/page-loaders/hybrid-page';
|
|
4
4
|
import type { PageTypes } from '@types';
|
|
5
|
+
import { getLatestServicePaths } from '../../../../services/_latest-version-route';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
// Services have their own route (pages/docs/services/[id]/[version]/changelog), because the
|
|
8
|
+
// version-less alias /docs/services/[id]/[docType]/[docId] outranks this generic route and
|
|
9
|
+
// would otherwise catch /docs/services/{id}/{version}/changelog.
|
|
10
|
+
const GENERIC_ROUTE_TYPES = changelogResourceCollections.filter((type) => type !== 'services');
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
type RouteParams = { type?: string; id?: string; version?: string };
|
|
13
|
+
|
|
14
|
+
const createChangelogPage = ({
|
|
15
|
+
types,
|
|
16
|
+
toParams,
|
|
17
|
+
typeOf,
|
|
18
|
+
}: {
|
|
19
|
+
types: PageTypes[];
|
|
20
|
+
toParams: (type: PageTypes, item: any) => RouteParams;
|
|
21
|
+
typeOf: (params: RouteParams) => string | undefined;
|
|
22
|
+
}) =>
|
|
23
|
+
class extends HybridPage {
|
|
24
|
+
static get prerender(): boolean {
|
|
25
|
+
return !isSSR();
|
|
14
26
|
}
|
|
15
27
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
protected static async fetchData(params: any) {
|
|
38
|
-
const { type, id, version } = params;
|
|
39
|
-
|
|
40
|
-
if (!type || !id || !version || !isChangelogEnabled()) {
|
|
41
|
-
return null;
|
|
28
|
+
static async getStaticPaths(): Promise<Array<{ params: any; props: any }>> {
|
|
29
|
+
if (isSSR() || !isChangelogEnabled()) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader');
|
|
34
|
+
|
|
35
|
+
const allItems = await Promise.all(types.map((type) => pageDataLoader[type]()));
|
|
36
|
+
|
|
37
|
+
return allItems.flatMap((items, index) =>
|
|
38
|
+
items.map((item) => ({
|
|
39
|
+
params: toParams(types[index], item),
|
|
40
|
+
props: {
|
|
41
|
+
type: types[index],
|
|
42
|
+
allCollectionItems: items,
|
|
43
|
+
...item,
|
|
44
|
+
},
|
|
45
|
+
}))
|
|
46
|
+
);
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
protected static async fetchData(params: RouteParams) {
|
|
50
|
+
const type = typeOf(params);
|
|
51
|
+
const { id, version } = params;
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
if (!type || !id || !version || !isChangelogEnabled()) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
const item = items.find((i) => i.data.id === id && i.data.version === version);
|
|
57
|
+
const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader');
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
const items = await pageDataLoader[type as PageTypes]();
|
|
60
|
+
const item = items.find((i) => i.data.id === id && i.data.version === version);
|
|
61
|
+
|
|
62
|
+
if (!item) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
type,
|
|
68
|
+
allCollectionItems: items,
|
|
69
|
+
...item,
|
|
70
|
+
};
|
|
54
71
|
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const Page = createChangelogPage({
|
|
75
|
+
types: GENERIC_ROUTE_TYPES as PageTypes[],
|
|
76
|
+
toParams: (type, item) => ({ type, id: item.data.id, version: item.data.version }),
|
|
77
|
+
typeOf: (params) => params.type,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const ServicePage = createChangelogPage({
|
|
81
|
+
types: ['services'],
|
|
82
|
+
toParams: (_type, item) => ({ id: item.data.id, version: item.data.version }),
|
|
83
|
+
typeOf: () => 'services',
|
|
84
|
+
});
|
|
55
85
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
86
|
+
// Static paths for the version-less /docs/services/[id]/changelog alias. The service route has no
|
|
87
|
+
// [type] segment, so put it back for getLatestServicePaths, which only keeps services.
|
|
88
|
+
export const getLatestServiceChangelogPaths = async () => {
|
|
89
|
+
const paths = await ServicePage.getStaticPaths();
|
|
90
|
+
return getLatestServicePaths(paths.map(({ params, props }) => ({ params: { ...params, type: 'services' }, props })));
|
|
91
|
+
};
|