@eventcatalog/core 4.10.15 → 4.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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-HBVTU6ZP.js → chunk-DWC5GRFX.js} +1 -1
- package/dist/{chunk-A4JRWRCV.js → chunk-H3324DNT.js} +1 -1
- package/dist/{chunk-I6HUOX4R.js → chunk-SW7SJZIX.js} +1 -1
- package/dist/{chunk-7LWOCX4R.js → chunk-W4LDIMUI.js} +1 -1
- package/dist/{chunk-Q2ATWE7N.js → chunk-XJOKK5ZO.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- 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/astro.config.mjs +2 -1
- package/eventcatalog/public/icons/languages/android.svg +1 -0
- package/eventcatalog/public/icons/languages/angular.svg +1 -0
- package/eventcatalog/public/icons/languages/apple.svg +1 -0
- package/eventcatalog/public/icons/languages/arduino.svg +1 -0
- package/eventcatalog/public/icons/languages/bash.svg +1 -0
- package/eventcatalog/public/icons/languages/bun.svg +1 -0
- package/eventcatalog/public/icons/languages/c.svg +1 -0
- package/eventcatalog/public/icons/languages/clojure.svg +1 -0
- package/eventcatalog/public/icons/languages/cpp.svg +1 -0
- package/eventcatalog/public/icons/languages/crystal.svg +1 -0
- package/eventcatalog/public/icons/languages/csharp.svg +1 -0
- package/eventcatalog/public/icons/languages/css.svg +1 -0
- package/eventcatalog/public/icons/languages/curl.svg +1 -0
- package/eventcatalog/public/icons/languages/dart.svg +1 -0
- package/eventcatalog/public/icons/languages/deno.svg +1 -0
- package/eventcatalog/public/icons/languages/docker.svg +1 -0
- package/eventcatalog/public/icons/languages/elm.svg +1 -0
- package/eventcatalog/public/icons/languages/erlang.svg +1 -0
- package/eventcatalog/public/icons/languages/flutter.svg +1 -0
- package/eventcatalog/public/icons/languages/fortran.svg +1 -0
- package/eventcatalog/public/icons/languages/fsharp.svg +1 -0
- package/eventcatalog/public/icons/languages/git.svg +1 -0
- package/eventcatalog/public/icons/languages/gradle.svg +1 -0
- package/eventcatalog/public/icons/languages/graphql.svg +1 -0
- package/eventcatalog/public/icons/languages/grpc.svg +1 -0
- package/eventcatalog/public/icons/languages/haskell.svg +1 -0
- package/eventcatalog/public/icons/languages/hasura.svg +1 -0
- package/eventcatalog/public/icons/languages/html.svg +1 -0
- package/eventcatalog/public/icons/languages/httpie.svg +1 -0
- package/eventcatalog/public/icons/languages/javascript.svg +1 -0
- package/eventcatalog/public/icons/languages/json-schema.svg +1 -0
- package/eventcatalog/public/icons/languages/json.svg +1 -0
- package/eventcatalog/public/icons/languages/julia.svg +1 -0
- package/eventcatalog/public/icons/languages/jupyter.svg +1 -0
- package/eventcatalog/public/icons/languages/kafka.svg +1 -0
- package/eventcatalog/public/icons/languages/kotlin.svg +1 -0
- package/eventcatalog/public/icons/languages/lua.svg +1 -0
- package/eventcatalog/public/icons/languages/markdown.svg +1 -0
- package/eventcatalog/public/icons/languages/maven.svg +1 -0
- package/eventcatalog/public/icons/languages/mongodb.svg +1 -0
- package/eventcatalog/public/icons/languages/mysql.svg +1 -0
- package/eventcatalog/public/icons/languages/nginx.svg +1 -0
- package/eventcatalog/public/icons/languages/nim.svg +1 -0
- package/eventcatalog/public/icons/languages/ocaml.svg +1 -0
- package/eventcatalog/public/icons/languages/perl.svg +1 -0
- package/eventcatalog/public/icons/languages/php.svg +1 -0
- package/eventcatalog/public/icons/languages/postgresql.svg +1 -0
- package/eventcatalog/public/icons/languages/prisma.svg +1 -0
- package/eventcatalog/public/icons/languages/purescript.svg +1 -0
- package/eventcatalog/public/icons/languages/python.svg +1 -0
- package/eventcatalog/public/icons/languages/r.svg +1 -0
- package/eventcatalog/public/icons/languages/react.svg +1 -0
- package/eventcatalog/public/icons/languages/reasonml.svg +1 -0
- package/eventcatalog/public/icons/languages/redis.svg +1 -0
- package/eventcatalog/public/icons/languages/ruby.svg +1 -0
- package/eventcatalog/public/icons/languages/rust.svg +1 -0
- package/eventcatalog/public/icons/languages/sass.svg +1 -0
- package/eventcatalog/public/icons/languages/scala.svg +1 -0
- package/eventcatalog/public/icons/languages/solidity.svg +1 -0
- package/eventcatalog/public/icons/languages/spring.svg +1 -0
- package/eventcatalog/public/icons/languages/sqlite.svg +1 -0
- package/eventcatalog/public/icons/languages/svelte.svg +1 -0
- package/eventcatalog/public/icons/languages/swagger.svg +1 -0
- package/eventcatalog/public/icons/languages/swift.svg +1 -0
- package/eventcatalog/public/icons/languages/tailwind.svg +1 -0
- package/eventcatalog/public/icons/languages/terraform.svg +1 -0
- package/eventcatalog/public/icons/languages/toml.svg +1 -0
- package/eventcatalog/public/icons/languages/typescript.svg +1 -0
- package/eventcatalog/public/icons/languages/unity.svg +1 -0
- package/eventcatalog/public/icons/languages/vim.svg +1 -0
- package/eventcatalog/public/icons/languages/vue.svg +1 -0
- package/eventcatalog/public/icons/languages/webassembly.svg +1 -0
- package/eventcatalog/public/icons/languages/yaml.svg +1 -0
- package/eventcatalog/public/icons/languages/zig.svg +1 -0
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.astro +12 -0
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +176 -0
- package/eventcatalog/src/components/MDX/CodeGroup/MarkdownCodeGroup.tsx +22 -0
- package/eventcatalog/src/components/MDX/CodeGroup/code-group-icons.ts +345 -0
- package/eventcatalog/src/components/MDX/CodeGroup/code-group.css +22 -0
- package/eventcatalog/src/components/MDX/CodeGroup/code-group.spec.tsx +89 -0
- package/eventcatalog/src/components/MDX/CodeGroup/parse-panels.ts +11 -0
- package/eventcatalog/src/components/MDX/Columns/Column.astro +10 -0
- package/eventcatalog/src/components/MDX/Columns/Columns.astro +14 -0
- package/eventcatalog/src/components/MDX/Columns/MarkdownColumns.tsx +37 -0
- package/eventcatalog/src/components/MDX/Columns/columns.spec.tsx +66 -0
- package/eventcatalog/src/components/MDX/Columns/layout.ts +16 -0
- package/eventcatalog/src/components/MDX/components.tsx +6 -0
- package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +1 -1
- package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +151 -114
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +510 -248
- package/eventcatalog/src/components/SchemaExplorer/SchemaFlows.tsx +76 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaGraph.tsx +47 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaPageViewer.tsx +11 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaUsage.tsx +17 -0
- package/eventcatalog/src/components/SchemaExplorer/types.ts +48 -3
- package/eventcatalog/src/components/SchemaExplorer/utils.ts +28 -1
- package/eventcatalog/src/content.config.ts +28 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +38 -7
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +144 -37
- package/eventcatalog/src/pages/schemas/explorer/index.astro +5 -0
- package/eventcatalog/src/remark-plugins/code-group.ts +39 -0
- package/eventcatalog/src/remark-plugins/markdown-code-group.ts +59 -0
- package/eventcatalog/src/remark-plugins/markdown-columns.ts +66 -0
- package/eventcatalog/src/utils/collections/examples.ts +133 -41
- package/eventcatalog/src/utils/collections/flows.ts +43 -0
- package/eventcatalog/src/utils/collections/messages.ts +2 -2
- package/eventcatalog/src/utils/collections/schemas.ts +15 -0
- package/eventcatalog/src/utils/schema-explorer.ts +73 -7
- package/eventcatalog/src/utils/schema-usage-graph.ts +92 -0
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo, useEffect, useRef } from 'react';
|
|
1
|
+
import { Fragment, useState, useMemo, useEffect, useRef } from 'react';
|
|
2
2
|
import * as Diff from 'diff';
|
|
3
3
|
import { html } from 'diff2html';
|
|
4
4
|
import 'diff2html/bundles/css/diff2html.min.css';
|
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
ClipboardDocumentIcon,
|
|
10
10
|
TableCellsIcon,
|
|
11
11
|
CodeBracketIcon,
|
|
12
|
-
ClockIcon,
|
|
13
12
|
GlobeAltIcon,
|
|
14
13
|
ServerIcon,
|
|
15
|
-
|
|
14
|
+
CodeBracketSquareIcon,
|
|
15
|
+
RectangleStackIcon,
|
|
16
|
+
ArrowsRightLeftIcon,
|
|
17
|
+
InformationCircleIcon,
|
|
16
18
|
} from '@heroicons/react/24/outline';
|
|
17
19
|
import { CheckIcon } from '@heroicons/react/20/solid';
|
|
18
20
|
import { buildUrl } from '@utils/url-builder';
|
|
@@ -21,13 +23,287 @@ import SchemaContentViewer from './SchemaContentViewer';
|
|
|
21
23
|
import DiffViewer from './DiffViewer';
|
|
22
24
|
import ApiContentViewer from './ApiContentViewer';
|
|
23
25
|
import ExamplesViewer from './ExamplesViewer';
|
|
26
|
+
import SchemaUsage from './SchemaUsage';
|
|
27
|
+
import SchemaFlows from './SchemaFlows';
|
|
24
28
|
import VersionHistoryModal from './VersionHistoryModal';
|
|
25
29
|
import SchemaCodeModal from './SchemaCodeModal';
|
|
26
30
|
import SchemaViewerModal from './SchemaViewerModal';
|
|
27
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
copyToClipboard,
|
|
33
|
+
downloadSchema,
|
|
34
|
+
getSchemaTypeLabel,
|
|
35
|
+
ICON_SPECS,
|
|
36
|
+
getSchemaRelationshipReference,
|
|
37
|
+
getSchemaRelationshipHref,
|
|
38
|
+
SCHEMA_RELATIONSHIP_LABELS,
|
|
39
|
+
} from './utils';
|
|
28
40
|
import { createSchemaDetailsLoader, useSchemaDetails } from './useSchemaDetails';
|
|
29
41
|
import { parseProtobufSchema } from '@utils/protobuf-schema';
|
|
30
|
-
import type { SchemaItem, VersionDiff, Owner, Producer
|
|
42
|
+
import type { SchemaItem, SchemaSourceInfo, SchemaRelationshipCollection, VersionDiff, Owner, Producer } from './types';
|
|
43
|
+
|
|
44
|
+
const MESSAGE_TYPE_LABELS: Partial<Record<SchemaItem['collection'], string>> = {
|
|
45
|
+
events: 'Event',
|
|
46
|
+
commands: 'Command',
|
|
47
|
+
queries: 'Query',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const SCHEMA_SOURCE_PROVIDER_LABELS: Record<string, string> = {
|
|
51
|
+
git: 'Git',
|
|
52
|
+
http: 'HTTP',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const getSchemaSourceLabel = (source: SchemaSourceInfo) => {
|
|
56
|
+
const provider = SCHEMA_SOURCE_PROVIDER_LABELS[source.provider] ?? source.provider;
|
|
57
|
+
return source.ref ? `${provider} (${source.ref})` : provider;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const formatSchemaUpdatedAt = (value: string) => {
|
|
61
|
+
const date = new Date(value);
|
|
62
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
63
|
+
return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type MetadataRow = { label: string; value: React.ReactNode; title?: string };
|
|
67
|
+
|
|
68
|
+
const MetadataEmpty = () => <span className="text-[rgb(var(--ec-page-text-muted)/0.6)]">-</span>;
|
|
69
|
+
|
|
70
|
+
const MetadataTable = ({ title, rows }: { title: string; rows: MetadataRow[] }) => (
|
|
71
|
+
<section className="overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border)/0.72)] bg-[rgb(var(--ec-dropdown-bg)/0.66)] dark:border-white/10">
|
|
72
|
+
<div className="border-b border-[rgb(var(--ec-page-border)/0.6)] bg-[rgb(var(--ec-content-hover)/0.45)] px-4 py-2.5 dark:border-white/10">
|
|
73
|
+
<h3 className="text-[11px] font-medium uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">{title}</h3>
|
|
74
|
+
</div>
|
|
75
|
+
<dl className="divide-y divide-[rgb(var(--ec-page-border)/0.5)] dark:divide-white/8">
|
|
76
|
+
{rows.map((row) => (
|
|
77
|
+
<div key={row.label} className="grid grid-cols-[10rem_1fr] gap-4 px-4 py-2.5 text-sm" title={row.title}>
|
|
78
|
+
<dt className="text-[rgb(var(--ec-page-text-muted))]">{row.label}</dt>
|
|
79
|
+
<dd className="min-w-0 break-words font-medium text-[rgb(var(--ec-page-text))]">{row.value}</dd>
|
|
80
|
+
</div>
|
|
81
|
+
))}
|
|
82
|
+
</dl>
|
|
83
|
+
</section>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const formatSchemaDateTime = (value: string) => {
|
|
87
|
+
const date = new Date(value);
|
|
88
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
89
|
+
return date.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const SchemaMetadata = ({ message, owners }: { message: SchemaItem; owners: Owner[] }) => {
|
|
93
|
+
const { color } = getCollectionStyles(message.collection);
|
|
94
|
+
const source = message.source;
|
|
95
|
+
const schemaRows: MetadataRow[] = [
|
|
96
|
+
{ label: 'Name', value: message.data.name },
|
|
97
|
+
...(message.schemaName && message.schemaName !== message.data.name ? [{ label: 'Schema', value: message.schemaName }] : []),
|
|
98
|
+
{ label: 'Format', value: getSchemaTypeLabel(message.schemaExtension) },
|
|
99
|
+
{
|
|
100
|
+
label: MESSAGE_TYPE_LABELS[message.collection] ? 'Message Type' : 'Resource',
|
|
101
|
+
value: (
|
|
102
|
+
<span className={`capitalize text-${color}-500`}>{MESSAGE_TYPE_LABELS[message.collection] ?? message.collection}</span>
|
|
103
|
+
),
|
|
104
|
+
},
|
|
105
|
+
{ label: 'Version', value: <span className="font-mono text-xs tabular-nums">v{message.data.version}</span> },
|
|
106
|
+
...(message.data.schemaPath
|
|
107
|
+
? [{ label: 'File', value: <span className="font-mono text-xs">{message.data.schemaPath}</span> }]
|
|
108
|
+
: []),
|
|
109
|
+
{
|
|
110
|
+
label: 'Created',
|
|
111
|
+
value: source?.createdAt ? formatSchemaDateTime(source.createdAt) : <MetadataEmpty />,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
label: 'Updated',
|
|
115
|
+
value: source?.updatedAt ? formatSchemaDateTime(source.updatedAt) : <MetadataEmpty />,
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const sourceRows: MetadataRow[] = source
|
|
120
|
+
? [
|
|
121
|
+
{ label: 'Source', value: getSchemaSourceLabel(source) },
|
|
122
|
+
...(message.schemaRef
|
|
123
|
+
? [{ label: 'Reference', value: <span className="font-mono text-xs">{message.schemaRef}</span> }]
|
|
124
|
+
: []),
|
|
125
|
+
...(source.path ? [{ label: 'Location', value: <span className="font-mono text-xs">{source.path}</span> }] : []),
|
|
126
|
+
...(source.repository
|
|
127
|
+
? [{ label: 'Repository', value: <span className="font-mono text-xs">{source.repository}</span> }]
|
|
128
|
+
: []),
|
|
129
|
+
...(source.commit ? [{ label: 'Commit', value: <span className="font-mono text-xs">{source.commit}</span> }] : []),
|
|
130
|
+
...(source.url
|
|
131
|
+
? [
|
|
132
|
+
{
|
|
133
|
+
label: 'Link',
|
|
134
|
+
value: (
|
|
135
|
+
<a
|
|
136
|
+
href={source.url}
|
|
137
|
+
target="_blank"
|
|
138
|
+
rel="noopener noreferrer"
|
|
139
|
+
className="inline-flex items-center gap-1 text-[rgb(var(--ec-accent))] hover:underline"
|
|
140
|
+
>
|
|
141
|
+
<span className="break-all">{source.url}</span>
|
|
142
|
+
<ArrowTopRightOnSquareIcon className="h-3 w-3 flex-shrink-0" />
|
|
143
|
+
</a>
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
]
|
|
147
|
+
: []),
|
|
148
|
+
]
|
|
149
|
+
: [{ label: 'Source', value: 'Local file' }];
|
|
150
|
+
|
|
151
|
+
const ownerRows: MetadataRow[] = [
|
|
152
|
+
{
|
|
153
|
+
label: owners.length === 1 ? 'Owner' : 'Owners',
|
|
154
|
+
value:
|
|
155
|
+
owners.length > 0 ? (
|
|
156
|
+
<span className="flex flex-wrap gap-x-3 gap-y-1">
|
|
157
|
+
{owners.map((owner, idx) => (
|
|
158
|
+
<a key={`${owner.id}-${idx}`} href={owner.href} className="hover:text-[rgb(var(--ec-accent))] hover:underline">
|
|
159
|
+
{owner.name}
|
|
160
|
+
</a>
|
|
161
|
+
))}
|
|
162
|
+
</span>
|
|
163
|
+
) : (
|
|
164
|
+
<MetadataEmpty />
|
|
165
|
+
),
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className="h-full space-y-4 overflow-auto pr-1">
|
|
171
|
+
<MetadataTable title="Schema" rows={schemaRows} />
|
|
172
|
+
<MetadataTable title="Source" rows={sourceRows} />
|
|
173
|
+
<MetadataTable title="Ownership" rows={ownerRows} />
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const SchemaVersionsTable = ({
|
|
179
|
+
versions,
|
|
180
|
+
currentVersion,
|
|
181
|
+
onSelect,
|
|
182
|
+
onCompare,
|
|
183
|
+
}: {
|
|
184
|
+
versions: SchemaItem[];
|
|
185
|
+
currentVersion: string;
|
|
186
|
+
onSelect: (version: string) => void;
|
|
187
|
+
/** Opens a diff between the given version and the one being viewed. */
|
|
188
|
+
onCompare?: (version: string) => void;
|
|
189
|
+
}) => (
|
|
190
|
+
<div className="overflow-auto rounded-xl border border-[rgb(var(--ec-page-border)/0.72)] bg-[rgb(var(--ec-dropdown-bg)/0.66)] dark:border-white/10">
|
|
191
|
+
<table className="min-w-full divide-y divide-[rgb(var(--ec-page-border)/0.62)] dark:divide-white/10">
|
|
192
|
+
<thead className="sticky top-0 z-10 bg-[rgb(var(--ec-content-hover)/0.45)]">
|
|
193
|
+
<tr>
|
|
194
|
+
<th className="px-4 py-2.5 text-left text-[11px] font-medium uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
|
|
195
|
+
Version
|
|
196
|
+
</th>
|
|
197
|
+
<th className="w-full px-4 py-2.5 text-left text-[11px] font-medium uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
|
|
198
|
+
Summary
|
|
199
|
+
</th>
|
|
200
|
+
<th className="px-4 py-2.5 text-left text-[11px] font-medium uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
|
|
201
|
+
Format
|
|
202
|
+
</th>
|
|
203
|
+
<th className="px-4 py-2.5 text-left text-[11px] font-medium uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
|
|
204
|
+
Updated
|
|
205
|
+
</th>
|
|
206
|
+
{onCompare && <th className="px-4 py-2.5" />}
|
|
207
|
+
</tr>
|
|
208
|
+
</thead>
|
|
209
|
+
<tbody className="divide-y divide-[rgb(var(--ec-page-border)/0.5)] dark:divide-white/8">
|
|
210
|
+
{versions.map((version, idx) => {
|
|
211
|
+
const isCurrent = version.data.version === currentVersion;
|
|
212
|
+
const isLatest = idx === 0;
|
|
213
|
+
return (
|
|
214
|
+
<tr
|
|
215
|
+
key={`${version.data.version}-${idx}`}
|
|
216
|
+
onClick={() => onSelect(version.data.version)}
|
|
217
|
+
className={`group cursor-pointer transition-colors ${
|
|
218
|
+
isCurrent
|
|
219
|
+
? 'bg-[rgb(var(--ec-accent-subtle)/0.55)]'
|
|
220
|
+
: 'bg-transparent hover:bg-[rgb(var(--ec-content-hover)/0.38)]'
|
|
221
|
+
}`}
|
|
222
|
+
>
|
|
223
|
+
<td className="whitespace-nowrap px-4 py-3 text-sm">
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onClick={(event) => {
|
|
227
|
+
event.stopPropagation();
|
|
228
|
+
onSelect(version.data.version);
|
|
229
|
+
}}
|
|
230
|
+
className="inline-flex items-center gap-2 text-left"
|
|
231
|
+
>
|
|
232
|
+
<span
|
|
233
|
+
className={`h-2 w-2 flex-shrink-0 rounded-full ${
|
|
234
|
+
isCurrent
|
|
235
|
+
? 'bg-[rgb(var(--ec-accent))]'
|
|
236
|
+
: 'bg-[rgb(var(--ec-page-border))] group-hover:bg-[rgb(var(--ec-page-text-muted))]'
|
|
237
|
+
}`}
|
|
238
|
+
/>
|
|
239
|
+
<span className="font-mono text-xs tabular-nums text-[rgb(var(--ec-page-text))]">v{version.data.version}</span>
|
|
240
|
+
{isLatest && (
|
|
241
|
+
<span className="rounded bg-[rgb(var(--ec-accent-subtle))] px-1.5 py-0.5 text-[10px] font-medium text-[rgb(var(--ec-accent))]">
|
|
242
|
+
latest
|
|
243
|
+
</span>
|
|
244
|
+
)}
|
|
245
|
+
{isCurrent && <span className="text-[10px] font-medium text-[rgb(var(--ec-page-text-muted))]">viewing</span>}
|
|
246
|
+
</button>
|
|
247
|
+
</td>
|
|
248
|
+
<td className="px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
249
|
+
{version.data.summary ? <p className="line-clamp-2 max-w-2xl">{version.data.summary}</p> : <span>-</span>}
|
|
250
|
+
</td>
|
|
251
|
+
<td className="whitespace-nowrap px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
252
|
+
{getSchemaTypeLabel(version.schemaExtension)}
|
|
253
|
+
</td>
|
|
254
|
+
<td className="whitespace-nowrap px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
255
|
+
{version.source?.updatedAt ? formatSchemaUpdatedAt(version.source.updatedAt) : '-'}
|
|
256
|
+
</td>
|
|
257
|
+
{onCompare && (
|
|
258
|
+
<td className="whitespace-nowrap px-4 py-3 text-right text-xs">
|
|
259
|
+
{!isCurrent && (
|
|
260
|
+
<button
|
|
261
|
+
type="button"
|
|
262
|
+
onClick={(event) => {
|
|
263
|
+
event.stopPropagation();
|
|
264
|
+
onCompare(version.data.version);
|
|
265
|
+
}}
|
|
266
|
+
className="font-medium text-[rgb(var(--ec-accent))] hover:underline"
|
|
267
|
+
>
|
|
268
|
+
Compare with v{currentVersion}
|
|
269
|
+
</button>
|
|
270
|
+
)}
|
|
271
|
+
</td>
|
|
272
|
+
)}
|
|
273
|
+
</tr>
|
|
274
|
+
);
|
|
275
|
+
})}
|
|
276
|
+
</tbody>
|
|
277
|
+
</table>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
type SchemaTabId = 'code' | 'schema' | 'api' | 'examples' | 'usage' | 'flows' | 'versions' | 'metadata';
|
|
282
|
+
|
|
283
|
+
/** Values used in the `tab` query parameter so a tab can be linked to directly. */
|
|
284
|
+
const SCHEMA_TAB_SLUGS: Record<SchemaTabId, string> = {
|
|
285
|
+
code: 'schema',
|
|
286
|
+
schema: 'properties',
|
|
287
|
+
metadata: 'details',
|
|
288
|
+
versions: 'versions',
|
|
289
|
+
examples: 'examples',
|
|
290
|
+
usage: 'producers-consumers',
|
|
291
|
+
flows: 'flows',
|
|
292
|
+
api: 'api',
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const DEFAULT_SCHEMA_TAB: SchemaTabId = 'code';
|
|
296
|
+
|
|
297
|
+
const getTabIdFromSlug = (slug: string | null): SchemaTabId | undefined =>
|
|
298
|
+
(Object.keys(SCHEMA_TAB_SLUGS) as SchemaTabId[]).find((id) => SCHEMA_TAB_SLUGS[id] === slug);
|
|
299
|
+
|
|
300
|
+
const writeTabToUrl = (tab: SchemaTabId) => {
|
|
301
|
+
if (typeof window === 'undefined') return;
|
|
302
|
+
const url = new URL(window.location.href);
|
|
303
|
+
if (tab === DEFAULT_SCHEMA_TAB) url.searchParams.delete('tab');
|
|
304
|
+
else url.searchParams.set('tab', SCHEMA_TAB_SLUGS[tab]);
|
|
305
|
+
window.history.replaceState(window.history.state, '', url);
|
|
306
|
+
};
|
|
31
307
|
|
|
32
308
|
interface SchemaDetailsPanelProps {
|
|
33
309
|
message: SchemaItem;
|
|
@@ -37,6 +313,10 @@ interface SchemaDetailsPanelProps {
|
|
|
37
313
|
apiAccessEnabled?: boolean;
|
|
38
314
|
showOwners?: boolean;
|
|
39
315
|
showProducersConsumers?: boolean;
|
|
316
|
+
/** Tab slug from the URL, when the server could read it. Lets the first render show the right tab. */
|
|
317
|
+
initialTab?: string;
|
|
318
|
+
/** Examples already rendered on the server (MDX with EventCatalog components). Falls back to Markdown rendering when absent. */
|
|
319
|
+
renderedExamples?: React.ReactNode;
|
|
40
320
|
}
|
|
41
321
|
|
|
42
322
|
export default function SchemaDetailsPanel({
|
|
@@ -47,26 +327,29 @@ export default function SchemaDetailsPanel({
|
|
|
47
327
|
apiAccessEnabled = false,
|
|
48
328
|
showOwners = true,
|
|
49
329
|
showProducersConsumers = true,
|
|
330
|
+
initialTab,
|
|
331
|
+
renderedExamples,
|
|
50
332
|
}: SchemaDetailsPanelProps) {
|
|
51
333
|
const [loadDetails] = useState(createSchemaDetailsLoader);
|
|
52
334
|
const content = useSchemaDetails(metadataMessage, loadDetails);
|
|
53
335
|
const message = content.message!;
|
|
54
336
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
55
|
-
const [activeTab, setActiveTab] = useState<
|
|
337
|
+
const [activeTab, setActiveTab] = useState<SchemaTabId>(() => getTabIdFromSlug(initialTab ?? null) ?? DEFAULT_SCHEMA_TAB);
|
|
56
338
|
const [isDiffModalOpen, setIsDiffModalOpen] = useState(false);
|
|
57
339
|
const [isCodeModalOpen, setIsCodeModalOpen] = useState(false);
|
|
58
340
|
const [isSchemaViewerModalOpen, setIsSchemaViewerModalOpen] = useState(false);
|
|
59
341
|
const [isActionsOpen, setIsActionsOpen] = useState(false);
|
|
60
342
|
const actionsMenuRef = useRef<HTMLDivElement>(null);
|
|
343
|
+
const [isVersionMenuOpen, setIsVersionMenuOpen] = useState(false);
|
|
344
|
+
const versionMenuRef = useRef<HTMLDivElement>(null);
|
|
61
345
|
|
|
62
|
-
const hasMultipleVersions = availableVersions.length > 1;
|
|
63
346
|
const { color } = getCollectionStyles(message.collection);
|
|
64
347
|
const ext = message.schemaExtension?.toLowerCase() || '';
|
|
65
348
|
const iconSpec = ICON_SPECS[ext];
|
|
66
349
|
const owners = message.data.owners || [];
|
|
67
350
|
const producers = message.data.producers || [];
|
|
68
351
|
const consumers = message.data.consumers || [];
|
|
69
|
-
const
|
|
352
|
+
const flows = message.data.flows || [];
|
|
70
353
|
|
|
71
354
|
const uniqueAvailableVersions = useMemo(
|
|
72
355
|
() =>
|
|
@@ -75,6 +358,8 @@ export default function SchemaDetailsPanel({
|
|
|
75
358
|
),
|
|
76
359
|
[availableVersions]
|
|
77
360
|
);
|
|
361
|
+
// Several schemas can share one message version, so count distinct versions only.
|
|
362
|
+
const hasMultipleVersions = uniqueAvailableVersions.length > 1;
|
|
78
363
|
const defaultToVersion = uniqueAvailableVersions[0]?.data.version || '';
|
|
79
364
|
const defaultFromVersion = uniqueAvailableVersions[1]?.data.version || defaultToVersion;
|
|
80
365
|
const [diffFromVersion, setDiffFromVersion] = useState(defaultFromVersion);
|
|
@@ -100,7 +385,12 @@ export default function SchemaDetailsPanel({
|
|
|
100
385
|
() => uniqueAvailableVersions.find((version) => version.data.version === diffToVersion),
|
|
101
386
|
[diffToVersion, uniqueAvailableVersions]
|
|
102
387
|
);
|
|
103
|
-
const comparing = activeTab === '
|
|
388
|
+
const comparing = (activeTab === 'versions' && hasMultipleVersions) || isDiffModalOpen;
|
|
389
|
+
|
|
390
|
+
const compareWithCurrent = (version: string) => {
|
|
391
|
+
setDiffFromVersion(version);
|
|
392
|
+
setDiffToVersion(message.data.version);
|
|
393
|
+
};
|
|
104
394
|
const fromContent = useSchemaDetails(diffFromMetadata, loadDetails, comparing);
|
|
105
395
|
const toContent = useSchemaDetails(diffToMetadata, loadDetails, comparing);
|
|
106
396
|
const diffFromItem = fromContent.message;
|
|
@@ -212,18 +502,21 @@ export default function SchemaDetailsPanel({
|
|
|
212
502
|
|
|
213
503
|
const isCopied = copiedId === message.data.id;
|
|
214
504
|
const docsUrl = buildUrl(`/docs/${message.collection}/${message.data.id}/${message.data.version}`);
|
|
215
|
-
const sidebarCardClass = 'rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg)/0.66)] px-4 py-4';
|
|
216
505
|
|
|
217
506
|
useEffect(() => {
|
|
218
507
|
const handlePointerDown = (event: MouseEvent) => {
|
|
219
508
|
if (!actionsMenuRef.current?.contains(event.target as Node)) {
|
|
220
509
|
setIsActionsOpen(false);
|
|
221
510
|
}
|
|
511
|
+
if (!versionMenuRef.current?.contains(event.target as Node)) {
|
|
512
|
+
setIsVersionMenuOpen(false);
|
|
513
|
+
}
|
|
222
514
|
};
|
|
223
515
|
|
|
224
516
|
const handleEscape = (event: KeyboardEvent) => {
|
|
225
517
|
if (event.key === 'Escape') {
|
|
226
518
|
setIsActionsOpen(false);
|
|
519
|
+
setIsVersionMenuOpen(false);
|
|
227
520
|
}
|
|
228
521
|
};
|
|
229
522
|
|
|
@@ -237,21 +530,63 @@ export default function SchemaDetailsPanel({
|
|
|
237
530
|
}, []);
|
|
238
531
|
|
|
239
532
|
const hasParsedSchema = !!parsedSchema || !!parsedAvroSchema || !!parsedProtoSchema;
|
|
240
|
-
// Build tabs
|
|
241
|
-
|
|
242
|
-
|
|
533
|
+
// Build tabs in three groups: the contract itself, who uses it, then reference material.
|
|
534
|
+
type SchemaTab = { id: SchemaTabId; label: string; icon: React.ReactNode; group: 'contract' | 'usage' | 'reference' };
|
|
535
|
+
const tabs: SchemaTab[] = [
|
|
536
|
+
{ id: 'code', label: 'Schema', icon: <CodeBracketIcon className="h-3.5 w-3.5" />, group: 'contract' },
|
|
243
537
|
];
|
|
244
538
|
if (hasParsedSchema) {
|
|
245
|
-
tabs.push({ id: 'schema', label: 'Properties', icon: <TableCellsIcon className="h-3.5 w-3.5"
|
|
539
|
+
tabs.push({ id: 'schema', label: 'Properties', icon: <TableCellsIcon className="h-3.5 w-3.5" />, group: 'contract' });
|
|
246
540
|
}
|
|
247
541
|
const examples = message.examples || [];
|
|
248
|
-
if (examples.length > 0) {
|
|
249
|
-
tabs.push({ id: 'examples', label: '
|
|
542
|
+
if (examples.length > 0 || renderedExamples) {
|
|
543
|
+
tabs.push({ id: 'examples', label: 'Examples', icon: <CodeBracketSquareIcon className="h-3.5 w-3.5" />, group: 'contract' });
|
|
250
544
|
}
|
|
251
|
-
|
|
252
|
-
|
|
545
|
+
const usageCount = producers.length + consumers.length;
|
|
546
|
+
const showUsageTab = showProducersConsumers && message.collection !== 'services' && usageCount > 0;
|
|
547
|
+
if (showUsageTab) {
|
|
548
|
+
tabs.push({
|
|
549
|
+
id: 'usage',
|
|
550
|
+
label: 'Producers & Consumers',
|
|
551
|
+
icon: <ServerIcon className="h-3.5 w-3.5" />,
|
|
552
|
+
group: 'usage',
|
|
553
|
+
});
|
|
253
554
|
}
|
|
254
|
-
|
|
555
|
+
const showFlowsTab = message.collection !== 'services' && flows.length > 0;
|
|
556
|
+
if (showFlowsTab) {
|
|
557
|
+
tabs.push({
|
|
558
|
+
id: 'flows',
|
|
559
|
+
label: `Flows (${flows.length})`,
|
|
560
|
+
icon: <ArrowsRightLeftIcon className="h-3.5 w-3.5" />,
|
|
561
|
+
group: 'usage',
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
tabs.push({
|
|
565
|
+
id: 'versions',
|
|
566
|
+
label: `Versions (${uniqueAvailableVersions.length})`,
|
|
567
|
+
icon: <RectangleStackIcon className="h-3.5 w-3.5" />,
|
|
568
|
+
group: 'reference',
|
|
569
|
+
});
|
|
570
|
+
tabs.push({ id: 'metadata', label: 'Details', icon: <InformationCircleIcon className="h-3.5 w-3.5" />, group: 'reference' });
|
|
571
|
+
tabs.push({ id: 'api', label: 'API', icon: <GlobeAltIcon className="h-3.5 w-3.5" />, group: 'reference' });
|
|
572
|
+
|
|
573
|
+
const availableTabIds = tabs.map((tab) => tab.id).join(',');
|
|
574
|
+
const selectTab = (tab: SchemaTabId) => {
|
|
575
|
+
setActiveTab(tab);
|
|
576
|
+
writeTabToUrl(tab);
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// Restore the tab named in the URL once the available tabs are known (static builds cannot resolve it on the
|
|
580
|
+
// server), and fall back to the default when the requested tab does not exist for this message.
|
|
581
|
+
useEffect(() => {
|
|
582
|
+
const available = availableTabIds.split(',');
|
|
583
|
+
const requested = getTabIdFromSlug(new URLSearchParams(window.location.search).get('tab'));
|
|
584
|
+
if (requested && available.includes(requested)) {
|
|
585
|
+
setActiveTab(requested);
|
|
586
|
+
} else {
|
|
587
|
+
setActiveTab((current) => (available.includes(current) ? current : DEFAULT_SCHEMA_TAB));
|
|
588
|
+
}
|
|
589
|
+
}, [availableTabIds]);
|
|
255
590
|
|
|
256
591
|
return (
|
|
257
592
|
<div className="flex h-full min-h-0 bg-[rgb(var(--ec-page-bg))] overflow-hidden">
|
|
@@ -270,14 +605,54 @@ export default function SchemaDetailsPanel({
|
|
|
270
605
|
)}
|
|
271
606
|
</div>
|
|
272
607
|
<h2 className="text-xl font-semibold text-[rgb(var(--ec-page-text))] truncate">{message.data.name}</h2>
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
608
|
+
<div className="relative flex-shrink-0" ref={versionMenuRef}>
|
|
609
|
+
<button
|
|
610
|
+
type="button"
|
|
611
|
+
onClick={() => setIsVersionMenuOpen((prev) => !prev)}
|
|
612
|
+
aria-expanded={isVersionMenuOpen}
|
|
613
|
+
aria-haspopup="listbox"
|
|
614
|
+
title="Switch version"
|
|
615
|
+
className="inline-flex items-center gap-1 rounded-md border border-transparent bg-[rgb(var(--ec-content-hover))] px-2 py-0.5 font-mono text-xs tabular-nums text-[rgb(var(--ec-page-text-muted))] transition-colors hover:border-[rgb(var(--ec-page-border))] hover:text-[rgb(var(--ec-page-text))]"
|
|
616
|
+
>
|
|
617
|
+
v{message.data.version}
|
|
618
|
+
<ChevronDownIcon className={`h-3 w-3 transition-transform ${isVersionMenuOpen ? 'rotate-180' : ''}`} />
|
|
619
|
+
</button>
|
|
620
|
+
{isVersionMenuOpen && (
|
|
621
|
+
<ul
|
|
622
|
+
role="listbox"
|
|
623
|
+
aria-label="Versions"
|
|
624
|
+
className="absolute left-0 top-[calc(100%+0.4rem)] z-20 max-h-72 min-w-[12rem] overflow-y-auto rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] py-1 shadow-xl"
|
|
625
|
+
>
|
|
626
|
+
{uniqueAvailableVersions.map((version, idx) => {
|
|
627
|
+
const isCurrent = version.data.version === message.data.version;
|
|
628
|
+
return (
|
|
629
|
+
<li key={`${version.data.version}-${idx}`} role="option" aria-selected={isCurrent}>
|
|
630
|
+
<button
|
|
631
|
+
type="button"
|
|
632
|
+
onClick={() => {
|
|
633
|
+
setIsVersionMenuOpen(false);
|
|
634
|
+
if (!isCurrent) onVersionChange(version.data.version);
|
|
635
|
+
}}
|
|
636
|
+
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-xs transition-colors hover:bg-[rgb(var(--ec-content-hover))] ${
|
|
637
|
+
isCurrent ? 'text-[rgb(var(--ec-page-text))]' : 'text-[rgb(var(--ec-page-text-muted))]'
|
|
638
|
+
}`}
|
|
639
|
+
>
|
|
640
|
+
<CheckIcon
|
|
641
|
+
className={`h-3.5 w-3.5 flex-shrink-0 ${isCurrent ? 'text-[rgb(var(--ec-accent))]' : 'invisible'}`}
|
|
642
|
+
/>
|
|
643
|
+
<span className="font-mono tabular-nums">v{version.data.version}</span>
|
|
644
|
+
{idx === 0 && (
|
|
645
|
+
<span className="ml-auto rounded bg-[rgb(var(--ec-accent-subtle))] px-1.5 py-0.5 text-[10px] font-medium text-[rgb(var(--ec-accent))]">
|
|
646
|
+
latest
|
|
647
|
+
</span>
|
|
648
|
+
)}
|
|
649
|
+
</button>
|
|
650
|
+
</li>
|
|
651
|
+
);
|
|
652
|
+
})}
|
|
653
|
+
</ul>
|
|
654
|
+
)}
|
|
655
|
+
</div>
|
|
281
656
|
</div>
|
|
282
657
|
{message.data.summary && (
|
|
283
658
|
<p className="mt-3 text-sm leading-relaxed text-[rgb(var(--ec-page-text-muted))]">{message.data.summary}</p>
|
|
@@ -349,19 +724,24 @@ export default function SchemaDetailsPanel({
|
|
|
349
724
|
{/* Tabs */}
|
|
350
725
|
<div className="flex-shrink-0 px-6">
|
|
351
726
|
<div className="flex items-center gap-1 border-b border-[rgb(var(--ec-page-border))]">
|
|
352
|
-
{tabs.map((tab) => (
|
|
353
|
-
<
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
727
|
+
{tabs.map((tab, index) => (
|
|
728
|
+
<Fragment key={tab.id}>
|
|
729
|
+
{index > 0 && tabs[index - 1].group !== tab.group && (
|
|
730
|
+
<span aria-hidden="true" className="mx-2 h-4 w-px bg-[rgb(var(--ec-page-border))]" />
|
|
731
|
+
)}
|
|
732
|
+
<button
|
|
733
|
+
onClick={() => selectTab(tab.id)}
|
|
734
|
+
data-tab-group={tab.group}
|
|
735
|
+
className={`inline-flex items-center gap-1.5 px-3 py-2 text-xs font-medium border-b-2 transition-colors ${
|
|
736
|
+
activeTab === tab.id
|
|
737
|
+
? 'border-[rgb(var(--ec-accent))] text-[rgb(var(--ec-page-text))]'
|
|
738
|
+
: 'border-transparent text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] hover:border-[rgb(var(--ec-page-border))]'
|
|
739
|
+
}`}
|
|
740
|
+
>
|
|
741
|
+
{tab.icon}
|
|
742
|
+
{tab.label}
|
|
743
|
+
</button>
|
|
744
|
+
</Fragment>
|
|
365
745
|
))}
|
|
366
746
|
</div>
|
|
367
747
|
</div>
|
|
@@ -370,8 +750,98 @@ export default function SchemaDetailsPanel({
|
|
|
370
750
|
<div className="flex-1 min-h-0 overflow-hidden p-6">
|
|
371
751
|
{content.loading || content.error ? (
|
|
372
752
|
<SchemaLoadingState loading={content.loading} error={content.error} retry={content.retry} />
|
|
753
|
+
) : activeTab === 'examples' && renderedExamples ? (
|
|
754
|
+
<div className="h-full overflow-auto pr-1">{renderedExamples}</div>
|
|
373
755
|
) : activeTab === 'examples' && examples.length > 0 ? (
|
|
374
756
|
<ExamplesViewer examples={examples} />
|
|
757
|
+
) : activeTab === 'metadata' ? (
|
|
758
|
+
<SchemaMetadata message={message} owners={showOwners ? owners : []} />
|
|
759
|
+
) : activeTab === 'versions' ? (
|
|
760
|
+
<div className="h-full space-y-4 overflow-auto pr-1">
|
|
761
|
+
<SchemaVersionsTable
|
|
762
|
+
versions={uniqueAvailableVersions}
|
|
763
|
+
currentVersion={message.data.version}
|
|
764
|
+
onSelect={onVersionChange}
|
|
765
|
+
onCompare={hasMultipleVersions ? compareWithCurrent : undefined}
|
|
766
|
+
/>
|
|
767
|
+
{hasMultipleVersions && (
|
|
768
|
+
<div className="flex flex-col">
|
|
769
|
+
<div className="mb-4 flex flex-shrink-0 flex-col gap-3 rounded-lg border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-content-hover)/0.45)] p-3 sm:flex-row sm:items-end sm:justify-between">
|
|
770
|
+
<div className="grid flex-1 grid-cols-1 gap-3 sm:grid-cols-2">
|
|
771
|
+
<label className="flex flex-col gap-1.5">
|
|
772
|
+
<span className="text-[11px] font-medium uppercase tracking-[0.14em] text-[rgb(var(--ec-page-text-muted))]">
|
|
773
|
+
From
|
|
774
|
+
</span>
|
|
775
|
+
<select
|
|
776
|
+
value={diffFromVersion}
|
|
777
|
+
onChange={(event) => setDiffFromVersion(event.target.value)}
|
|
778
|
+
className="h-9 rounded-md border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-sm font-mono tabular-nums text-[rgb(var(--ec-page-text))] outline-hidden transition-colors focus:border-[rgb(var(--ec-accent))] focus:ring-1 focus:ring-[rgb(var(--ec-accent)/0.3)]"
|
|
779
|
+
>
|
|
780
|
+
{uniqueAvailableVersions.map((version) => (
|
|
781
|
+
<option key={`from-${version.data.version}`} value={version.data.version}>
|
|
782
|
+
v{version.data.version}
|
|
783
|
+
</option>
|
|
784
|
+
))}
|
|
785
|
+
</select>
|
|
786
|
+
</label>
|
|
787
|
+
<label className="flex flex-col gap-1.5">
|
|
788
|
+
<span className="text-[11px] font-medium uppercase tracking-[0.14em] text-[rgb(var(--ec-page-text-muted))]">
|
|
789
|
+
To
|
|
790
|
+
</span>
|
|
791
|
+
<select
|
|
792
|
+
value={diffToVersion}
|
|
793
|
+
onChange={(event) => setDiffToVersion(event.target.value)}
|
|
794
|
+
className="h-9 rounded-md border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-sm font-mono tabular-nums text-[rgb(var(--ec-page-text))] outline-hidden transition-colors focus:border-[rgb(var(--ec-accent))] focus:ring-1 focus:ring-[rgb(var(--ec-accent)/0.3)]"
|
|
795
|
+
>
|
|
796
|
+
{uniqueAvailableVersions.map((version) => (
|
|
797
|
+
<option key={`to-${version.data.version}`} value={version.data.version}>
|
|
798
|
+
v{version.data.version}
|
|
799
|
+
</option>
|
|
800
|
+
))}
|
|
801
|
+
</select>
|
|
802
|
+
</label>
|
|
803
|
+
</div>
|
|
804
|
+
<div className="flex items-center gap-2">
|
|
805
|
+
<button
|
|
806
|
+
type="button"
|
|
807
|
+
onClick={() => setIsDiffModalOpen(true)}
|
|
808
|
+
disabled={!selectedDiff}
|
|
809
|
+
className="inline-flex h-9 items-center justify-center gap-1.5 rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-page-text))] disabled:cursor-not-allowed disabled:opacity-50"
|
|
810
|
+
>
|
|
811
|
+
<ArrowTopRightOnSquareIcon className="h-3.5 w-3.5" />
|
|
812
|
+
Expand
|
|
813
|
+
</button>
|
|
814
|
+
</div>
|
|
815
|
+
</div>
|
|
816
|
+
<div className="min-h-[16rem] overflow-hidden rounded-lg border border-[rgb(var(--ec-page-border))]">
|
|
817
|
+
{fromContent.loading || toContent.loading || fromContent.error || toContent.error ? (
|
|
818
|
+
<SchemaLoadingState
|
|
819
|
+
loading={fromContent.loading || toContent.loading}
|
|
820
|
+
error={fromContent.error || toContent.error}
|
|
821
|
+
retry={() => {
|
|
822
|
+
fromContent.retry();
|
|
823
|
+
toContent.retry();
|
|
824
|
+
}}
|
|
825
|
+
/>
|
|
826
|
+
) : diffFromVersion === diffToVersion ? (
|
|
827
|
+
<div className="flex h-full items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
|
|
828
|
+
<p className="text-sm">Select two different versions</p>
|
|
829
|
+
</div>
|
|
830
|
+
) : !hasDiffFromContent || !hasDiffToContent ? (
|
|
831
|
+
<div className="flex h-full items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
|
|
832
|
+
<p className="text-sm">No schema content available</p>
|
|
833
|
+
</div>
|
|
834
|
+
) : (
|
|
835
|
+
<DiffViewer diffs={selectedDiffs} apiAccessEnabled={apiAccessEnabled} />
|
|
836
|
+
)}
|
|
837
|
+
</div>
|
|
838
|
+
</div>
|
|
839
|
+
)}
|
|
840
|
+
</div>
|
|
841
|
+
) : activeTab === 'usage' && showUsageTab ? (
|
|
842
|
+
<SchemaUsage message={message} />
|
|
843
|
+
) : activeTab === 'flows' && showFlowsTab ? (
|
|
844
|
+
<SchemaFlows message={message} flows={flows} />
|
|
375
845
|
) : activeTab === 'api' ? (
|
|
376
846
|
<ApiContentViewer
|
|
377
847
|
message={message}
|
|
@@ -379,76 +849,6 @@ export default function SchemaDetailsPanel({
|
|
|
379
849
|
copiedId={copiedId}
|
|
380
850
|
apiAccessEnabled={apiAccessEnabled}
|
|
381
851
|
/>
|
|
382
|
-
) : activeTab === 'diff' && hasMultipleVersions ? (
|
|
383
|
-
<div className="flex h-full min-h-0 flex-col overflow-hidden">
|
|
384
|
-
<div className="mb-4 flex flex-shrink-0 flex-col gap-3 rounded-lg border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-content-hover)/0.45)] p-3 sm:flex-row sm:items-end sm:justify-between">
|
|
385
|
-
<div className="grid flex-1 grid-cols-1 gap-3 sm:grid-cols-2">
|
|
386
|
-
<label className="flex flex-col gap-1.5">
|
|
387
|
-
<span className="text-[11px] font-medium uppercase tracking-[0.14em] text-[rgb(var(--ec-page-text-muted))]">
|
|
388
|
-
From
|
|
389
|
-
</span>
|
|
390
|
-
<select
|
|
391
|
-
value={diffFromVersion}
|
|
392
|
-
onChange={(event) => setDiffFromVersion(event.target.value)}
|
|
393
|
-
className="h-9 rounded-md border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-sm font-mono tabular-nums text-[rgb(var(--ec-page-text))] outline-hidden transition-colors focus:border-[rgb(var(--ec-accent))] focus:ring-1 focus:ring-[rgb(var(--ec-accent)/0.3)]"
|
|
394
|
-
>
|
|
395
|
-
{uniqueAvailableVersions.map((version) => (
|
|
396
|
-
<option key={`from-${version.data.version}`} value={version.data.version}>
|
|
397
|
-
v{version.data.version}
|
|
398
|
-
</option>
|
|
399
|
-
))}
|
|
400
|
-
</select>
|
|
401
|
-
</label>
|
|
402
|
-
<label className="flex flex-col gap-1.5">
|
|
403
|
-
<span className="text-[11px] font-medium uppercase tracking-[0.14em] text-[rgb(var(--ec-page-text-muted))]">
|
|
404
|
-
To
|
|
405
|
-
</span>
|
|
406
|
-
<select
|
|
407
|
-
value={diffToVersion}
|
|
408
|
-
onChange={(event) => setDiffToVersion(event.target.value)}
|
|
409
|
-
className="h-9 rounded-md border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-sm font-mono tabular-nums text-[rgb(var(--ec-page-text))] outline-hidden transition-colors focus:border-[rgb(var(--ec-accent))] focus:ring-1 focus:ring-[rgb(var(--ec-accent)/0.3)]"
|
|
410
|
-
>
|
|
411
|
-
{uniqueAvailableVersions.map((version) => (
|
|
412
|
-
<option key={`to-${version.data.version}`} value={version.data.version}>
|
|
413
|
-
v{version.data.version}
|
|
414
|
-
</option>
|
|
415
|
-
))}
|
|
416
|
-
</select>
|
|
417
|
-
</label>
|
|
418
|
-
</div>
|
|
419
|
-
<button
|
|
420
|
-
type="button"
|
|
421
|
-
onClick={() => setIsDiffModalOpen(true)}
|
|
422
|
-
disabled={!selectedDiff}
|
|
423
|
-
className="inline-flex h-9 items-center justify-center gap-1.5 rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-page-text))] disabled:cursor-not-allowed disabled:opacity-50"
|
|
424
|
-
>
|
|
425
|
-
<ArrowTopRightOnSquareIcon className="h-3.5 w-3.5" />
|
|
426
|
-
Expand
|
|
427
|
-
</button>
|
|
428
|
-
</div>
|
|
429
|
-
<div className="min-h-0 flex-1 overflow-hidden">
|
|
430
|
-
{fromContent.loading || toContent.loading || fromContent.error || toContent.error ? (
|
|
431
|
-
<SchemaLoadingState
|
|
432
|
-
loading={fromContent.loading || toContent.loading}
|
|
433
|
-
error={fromContent.error || toContent.error}
|
|
434
|
-
retry={() => {
|
|
435
|
-
fromContent.retry();
|
|
436
|
-
toContent.retry();
|
|
437
|
-
}}
|
|
438
|
-
/>
|
|
439
|
-
) : diffFromVersion === diffToVersion ? (
|
|
440
|
-
<div className="flex h-full items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
|
|
441
|
-
<p className="text-sm">Select two different versions</p>
|
|
442
|
-
</div>
|
|
443
|
-
) : !hasDiffFromContent || !hasDiffToContent ? (
|
|
444
|
-
<div className="flex h-full items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
|
|
445
|
-
<p className="text-sm">No schema content available</p>
|
|
446
|
-
</div>
|
|
447
|
-
) : (
|
|
448
|
-
<DiffViewer diffs={selectedDiffs} apiAccessEnabled={apiAccessEnabled} />
|
|
449
|
-
)}
|
|
450
|
-
</div>
|
|
451
|
-
</div>
|
|
452
852
|
) : (
|
|
453
853
|
<SchemaContentViewer
|
|
454
854
|
message={message}
|
|
@@ -471,144 +871,6 @@ export default function SchemaDetailsPanel({
|
|
|
471
871
|
</div>
|
|
472
872
|
</div>
|
|
473
873
|
|
|
474
|
-
{/* Right sidebar - spans full height */}
|
|
475
|
-
<div className="h-full w-80 flex-shrink-0 overflow-y-auto border-l border-[rgb(var(--ec-page-border))] px-3 py-4">
|
|
476
|
-
<div className="space-y-3">
|
|
477
|
-
{/* Details section */}
|
|
478
|
-
<div className={sidebarCardClass}>
|
|
479
|
-
<h3 className="mb-4 text-[0.8rem] font-semibold text-[rgb(var(--ec-page-text))]">Details</h3>
|
|
480
|
-
<dl className="space-y-2.5">
|
|
481
|
-
<div className="flex items-center justify-between">
|
|
482
|
-
<dt className="text-xs text-[rgb(var(--ec-page-text-muted))]">Format</dt>
|
|
483
|
-
<dd className="text-xs font-medium text-[rgb(var(--ec-page-text-muted))]">
|
|
484
|
-
{getSchemaTypeLabel(message.schemaExtension)}
|
|
485
|
-
</dd>
|
|
486
|
-
</div>
|
|
487
|
-
<div className="flex items-center justify-between">
|
|
488
|
-
<dt className="text-xs text-[rgb(var(--ec-page-text-muted))]">Resource</dt>
|
|
489
|
-
<dd className={`text-xs font-medium text-${color}-400 capitalize`}>{message.collection}</dd>
|
|
490
|
-
</div>
|
|
491
|
-
<div className="flex items-center justify-between gap-3">
|
|
492
|
-
<dt className="text-xs text-[rgb(var(--ec-page-text-muted))]">Filename</dt>
|
|
493
|
-
<dd className="max-w-[11rem] truncate text-xs font-medium text-[rgb(var(--ec-page-text-muted))]" title={filename}>
|
|
494
|
-
{filename}
|
|
495
|
-
</dd>
|
|
496
|
-
</div>
|
|
497
|
-
</dl>
|
|
498
|
-
</div>
|
|
499
|
-
|
|
500
|
-
{/* Versions section */}
|
|
501
|
-
<div className={sidebarCardClass}>
|
|
502
|
-
<div className="mb-3 flex items-center justify-between gap-3">
|
|
503
|
-
<h3 className="text-[0.8rem] font-semibold text-[rgb(var(--ec-page-text))]">Versions</h3>
|
|
504
|
-
</div>
|
|
505
|
-
<div className="space-y-1">
|
|
506
|
-
{availableVersions
|
|
507
|
-
.filter((v, idx, arr) => arr.findIndex((a) => a.data.version === v.data.version) === idx)
|
|
508
|
-
.map((v, idx) => {
|
|
509
|
-
const isActive = v.data.version === message.data.version;
|
|
510
|
-
const isLatest = idx === 0;
|
|
511
|
-
return (
|
|
512
|
-
<button
|
|
513
|
-
key={`${v.data.version}-${idx}`}
|
|
514
|
-
onClick={() => onVersionChange(v.data.version)}
|
|
515
|
-
className={`w-full flex items-center gap-2 rounded-lg px-2 py-1.5 text-left transition-colors ${
|
|
516
|
-
isActive
|
|
517
|
-
? 'bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-page-text))]'
|
|
518
|
-
: 'text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-page-bg)/0.55)]'
|
|
519
|
-
}`}
|
|
520
|
-
>
|
|
521
|
-
<div
|
|
522
|
-
className={`w-2 h-2 rounded-full flex-shrink-0 ${
|
|
523
|
-
isActive ? 'bg-[rgb(var(--ec-accent))]' : 'bg-[rgb(var(--ec-page-border))]'
|
|
524
|
-
}`}
|
|
525
|
-
/>
|
|
526
|
-
<span className="text-xs font-mono tabular-nums">v{v.data.version}</span>
|
|
527
|
-
{isLatest && (
|
|
528
|
-
<span className="text-[10px] font-medium text-[rgb(var(--ec-accent))] bg-[rgb(var(--ec-accent-subtle))] px-1.5 py-0.5 rounded">
|
|
529
|
-
latest
|
|
530
|
-
</span>
|
|
531
|
-
)}
|
|
532
|
-
</button>
|
|
533
|
-
);
|
|
534
|
-
})}
|
|
535
|
-
</div>
|
|
536
|
-
</div>
|
|
537
|
-
|
|
538
|
-
{/* Schema producers section */}
|
|
539
|
-
{showProducersConsumers && producers.length > 0 && message.collection !== 'services' && (
|
|
540
|
-
<div className={sidebarCardClass}>
|
|
541
|
-
<div className="mb-2.5 flex items-center justify-between gap-3">
|
|
542
|
-
<h3 className="text-[0.8rem] font-semibold text-[rgb(var(--ec-page-text))]">
|
|
543
|
-
Schema Producers ({producers.length})
|
|
544
|
-
</h3>
|
|
545
|
-
</div>
|
|
546
|
-
<div className="space-y-1">
|
|
547
|
-
{producers.map((producer: Producer, idx: number) => {
|
|
548
|
-
const { id: serviceName, version } = getSchemaRelationshipReference(producer);
|
|
549
|
-
return (
|
|
550
|
-
<a
|
|
551
|
-
key={`${producer.id}-${idx}`}
|
|
552
|
-
href={buildUrl(`/docs/services/${serviceName}/${version}`)}
|
|
553
|
-
className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-page-bg)/0.55)] hover:text-[rgb(var(--ec-accent))]"
|
|
554
|
-
>
|
|
555
|
-
<ServerIcon className="h-3.5 w-3.5 flex-shrink-0 text-[rgb(var(--ec-page-text-muted))]" />
|
|
556
|
-
{serviceName}
|
|
557
|
-
</a>
|
|
558
|
-
);
|
|
559
|
-
})}
|
|
560
|
-
</div>
|
|
561
|
-
</div>
|
|
562
|
-
)}
|
|
563
|
-
|
|
564
|
-
{/* Schema consumers section */}
|
|
565
|
-
{showProducersConsumers && consumers.length > 0 && message.collection !== 'services' && (
|
|
566
|
-
<div className={sidebarCardClass}>
|
|
567
|
-
<div className="mb-2.5 flex items-center justify-between gap-3">
|
|
568
|
-
<h3 className="text-[0.8rem] font-semibold text-[rgb(var(--ec-page-text))]">
|
|
569
|
-
Schema Consumers ({consumers.length})
|
|
570
|
-
</h3>
|
|
571
|
-
</div>
|
|
572
|
-
<div className="space-y-1">
|
|
573
|
-
{consumers.map((consumer: Consumer, idx: number) => {
|
|
574
|
-
const { id: serviceName, version } = getSchemaRelationshipReference(consumer);
|
|
575
|
-
return (
|
|
576
|
-
<a
|
|
577
|
-
key={`${consumer.id}-${idx}`}
|
|
578
|
-
href={buildUrl(`/docs/services/${serviceName}/${version}`)}
|
|
579
|
-
className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-page-bg)/0.55)] hover:text-[rgb(var(--ec-accent))]"
|
|
580
|
-
>
|
|
581
|
-
<ServerIcon className="h-3.5 w-3.5 flex-shrink-0 text-[rgb(var(--ec-page-text-muted))]" />
|
|
582
|
-
{serviceName}
|
|
583
|
-
</a>
|
|
584
|
-
);
|
|
585
|
-
})}
|
|
586
|
-
</div>
|
|
587
|
-
</div>
|
|
588
|
-
)}
|
|
589
|
-
|
|
590
|
-
{/* Owners section */}
|
|
591
|
-
{showOwners && owners.length > 0 && (
|
|
592
|
-
<div className={sidebarCardClass}>
|
|
593
|
-
<div className="mb-3 flex items-center justify-between gap-3">
|
|
594
|
-
<h3 className="text-[0.8rem] font-semibold text-[rgb(var(--ec-page-text))]">Owners</h3>
|
|
595
|
-
</div>
|
|
596
|
-
<div className="space-y-1">
|
|
597
|
-
{owners.map((owner: Owner, idx: number) => (
|
|
598
|
-
<a
|
|
599
|
-
key={`${owner.id}-${idx}`}
|
|
600
|
-
href={owner.href}
|
|
601
|
-
className="block rounded-lg px-2 py-1.5 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-page-bg)/0.55)] hover:text-[rgb(var(--ec-accent))]"
|
|
602
|
-
>
|
|
603
|
-
{owner.name}
|
|
604
|
-
</a>
|
|
605
|
-
))}
|
|
606
|
-
</div>
|
|
607
|
-
</div>
|
|
608
|
-
)}
|
|
609
|
-
</div>
|
|
610
|
-
</div>
|
|
611
|
-
|
|
612
874
|
{/* Modals */}
|
|
613
875
|
<VersionHistoryModal
|
|
614
876
|
isOpen={isDiffModalOpen}
|