@eventcatalog/core 3.0.0-beta.26 → 3.0.0-beta.28
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-3YJD7KVJ.js → chunk-2GYNBYSC.js} +1 -1
- package/dist/{chunk-5TB5SKXE.js → chunk-3636UG43.js} +1 -1
- package/dist/{chunk-6VKMP3FH.js → chunk-BWYHQBO3.js} +1 -1
- package/dist/{chunk-MZTNQHMI.js → chunk-KZ2X6LDN.js} +1 -1
- package/dist/{chunk-WQFW32XA.js → chunk-TCUGAKA2.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/src/components/Grids/DomainGrid.tsx +309 -170
- package/eventcatalog/src/components/Grids/MessageGrid.tsx +301 -182
- package/eventcatalog/src/components/Grids/specification-utils.ts +106 -0
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +8 -2
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +9 -5
- package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +1 -1
- package/eventcatalog/src/stores/sidebar-store/builders/service.ts +1 -1
- package/eventcatalog/src/utils/feature.ts +1 -1
- package/package.json +1 -1
|
@@ -8,10 +8,13 @@ import {
|
|
|
8
8
|
CircleStackIcon,
|
|
9
9
|
ChevronDownIcon,
|
|
10
10
|
ChevronUpIcon,
|
|
11
|
-
|
|
11
|
+
ArrowTopRightOnSquareIcon,
|
|
12
|
+
ArrowLongRightIcon,
|
|
13
|
+
ArrowLongLeftIcon,
|
|
12
14
|
} from '@heroicons/react/24/outline';
|
|
13
15
|
import { buildUrl } from '@utils/url-builder';
|
|
14
16
|
import { BoxIcon } from 'lucide-react';
|
|
17
|
+
import { getSpecUrl, getSpecIcon, getSpecLabel, getServiceSpecifications } from './specification-utils';
|
|
15
18
|
|
|
16
19
|
// ============================================
|
|
17
20
|
// Types
|
|
@@ -32,14 +35,14 @@ const getMessageIcon = (collection: string) => {
|
|
|
32
35
|
case 'commands':
|
|
33
36
|
return { Icon: ChatBubbleLeftIcon, color: 'blue' };
|
|
34
37
|
case 'queries':
|
|
35
|
-
return { Icon: MagnifyingGlassIcon, color: '
|
|
38
|
+
return { Icon: MagnifyingGlassIcon, color: 'emerald' };
|
|
36
39
|
default:
|
|
37
40
|
return { Icon: BoltIcon, color: 'gray' };
|
|
38
41
|
}
|
|
39
42
|
};
|
|
40
43
|
|
|
41
44
|
// ============================================
|
|
42
|
-
//
|
|
45
|
+
// Sub-components
|
|
43
46
|
// ============================================
|
|
44
47
|
|
|
45
48
|
const EntityBadge = memo(({ entity }: { entity: any }) => {
|
|
@@ -49,175 +52,269 @@ const EntityBadge = memo(({ entity }: { entity: any }) => {
|
|
|
49
52
|
return (
|
|
50
53
|
<a
|
|
51
54
|
href={buildUrl(`/docs/entities/${id}`)}
|
|
52
|
-
className="inline-flex items-center gap-
|
|
55
|
+
className="inline-flex items-center gap-2 px-3 py-2 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 hover:border-gray-300 transition-all shadow-sm"
|
|
53
56
|
>
|
|
54
|
-
<BoxIcon className="h-
|
|
55
|
-
<span
|
|
57
|
+
<BoxIcon className="h-4 w-4 text-purple-500" />
|
|
58
|
+
<span>{name}</span>
|
|
56
59
|
</a>
|
|
57
60
|
);
|
|
58
61
|
});
|
|
59
62
|
|
|
60
|
-
const
|
|
63
|
+
const MessageLink = memo(({ message }: { message: any }) => {
|
|
61
64
|
const data = message?.data || message;
|
|
62
65
|
const collection = message?.collection || 'events';
|
|
63
66
|
const { Icon, color } = getMessageIcon(collection);
|
|
64
67
|
const id = data?.id || message?.id;
|
|
65
68
|
const name = data?.name || data?.id || id;
|
|
66
|
-
const version = data?.version;
|
|
69
|
+
const version = data?.version || message?.data?.version || 'latest';
|
|
70
|
+
|
|
71
|
+
const iconStyles: Record<string, string> = {
|
|
72
|
+
orange: 'text-orange-500',
|
|
73
|
+
blue: 'text-blue-500',
|
|
74
|
+
emerald: 'text-emerald-500',
|
|
75
|
+
gray: 'text-gray-500',
|
|
76
|
+
};
|
|
67
77
|
|
|
68
78
|
return (
|
|
69
79
|
<a
|
|
70
80
|
href={buildUrl(`/docs/${collection}/${id}/${version}`)}
|
|
71
|
-
className="flex items-center gap-
|
|
81
|
+
className="flex items-center gap-2 py-1.5 text-sm text-gray-700 hover:text-gray-900 transition-colors group"
|
|
72
82
|
>
|
|
73
|
-
<Icon className={`h-
|
|
74
|
-
<span className="
|
|
83
|
+
<Icon className={`h-4 w-4 flex-shrink-0 ${iconStyles[color]}`} />
|
|
84
|
+
<span className="group-hover:underline">{name}</span>
|
|
85
|
+
<span className="text-xs text-gray-400">v{version}</span>
|
|
75
86
|
</a>
|
|
76
87
|
);
|
|
77
88
|
});
|
|
78
89
|
|
|
79
|
-
const
|
|
90
|
+
const SpecificationBadge = memo(
|
|
91
|
+
({ spec, serviceId, serviceVersion }: { spec: any; serviceId: string; serviceVersion: string }) => {
|
|
92
|
+
return (
|
|
93
|
+
<a
|
|
94
|
+
href={getSpecUrl(spec, serviceId, serviceVersion)}
|
|
95
|
+
className="inline-flex items-center gap-1.5 px-2 py-1.5 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-700 hover:bg-gray-50 hover:border-gray-300 transition-all shadow-sm"
|
|
96
|
+
>
|
|
97
|
+
<img src={buildUrl(`/icons/${getSpecIcon(spec.type)}.svg`, true)} alt={`${spec.type} icon`} className="h-3.5 w-3.5" />
|
|
98
|
+
<span>{getSpecLabel(spec.type)}</span>
|
|
99
|
+
</a>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const ContainerLink = memo(({ container, type }: { container: any; type: 'reads' | 'writes' }) => {
|
|
80
105
|
const data = container?.data || container;
|
|
81
106
|
const id = data?.id || container?.id;
|
|
82
107
|
const name = data?.name || id;
|
|
83
|
-
const version = data?.version;
|
|
84
|
-
const colorClass = type === 'reads' ? 'orange' : 'purple';
|
|
108
|
+
const version = data?.version || 'latest';
|
|
85
109
|
|
|
86
110
|
return (
|
|
87
111
|
<a
|
|
88
112
|
href={buildUrl(`/docs/containers/${id}/${version}`)}
|
|
89
|
-
className=
|
|
113
|
+
className="flex items-center gap-2 py-1.5 text-sm text-gray-700 hover:text-gray-900 transition-colors group"
|
|
90
114
|
>
|
|
91
|
-
<CircleStackIcon className={`h-
|
|
92
|
-
<span className=
|
|
115
|
+
<CircleStackIcon className={`h-4 w-4 ${type === 'reads' ? 'text-amber-500' : 'text-violet-500'}`} />
|
|
116
|
+
<span className="group-hover:underline">{name}</span>
|
|
93
117
|
</a>
|
|
94
118
|
);
|
|
95
119
|
});
|
|
96
120
|
|
|
121
|
+
// Searchable scrollable box component
|
|
122
|
+
const SearchableBox = memo(
|
|
123
|
+
({
|
|
124
|
+
title,
|
|
125
|
+
icon: Icon,
|
|
126
|
+
iconColor,
|
|
127
|
+
items,
|
|
128
|
+
renderItem,
|
|
129
|
+
emptyText = '—',
|
|
130
|
+
}: {
|
|
131
|
+
title: string;
|
|
132
|
+
icon: any;
|
|
133
|
+
iconColor: string;
|
|
134
|
+
items: any[];
|
|
135
|
+
renderItem: (item: any, idx: number) => React.ReactNode;
|
|
136
|
+
emptyText?: string;
|
|
137
|
+
}) => {
|
|
138
|
+
const [search, setSearch] = useState('');
|
|
139
|
+
|
|
140
|
+
const filteredItems = items.filter((item) => {
|
|
141
|
+
if (!search) return true;
|
|
142
|
+
const data = item?.data || item;
|
|
143
|
+
const name = data?.name || data?.id || '';
|
|
144
|
+
return name.toLowerCase().includes(search.toLowerCase());
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="flex flex-col">
|
|
149
|
+
<div className="flex items-center gap-2 mb-2">
|
|
150
|
+
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
151
|
+
<Icon className={`h-4 w-4 ${iconColor}`} />
|
|
152
|
+
<h4 className="text-xs font-semibold text-gray-700 uppercase tracking-wide">{title}</h4>
|
|
153
|
+
<span className="text-[10px] text-gray-400 font-medium">({items.length})</span>
|
|
154
|
+
</div>
|
|
155
|
+
{items.length > 0 && (
|
|
156
|
+
<input
|
|
157
|
+
type="text"
|
|
158
|
+
placeholder="Search..."
|
|
159
|
+
value={search}
|
|
160
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
161
|
+
className="flex-1 px-2 py-0.5 text-xs border border-gray-200 rounded focus:outline-none focus:border-gray-300"
|
|
162
|
+
onClick={(e) => e.stopPropagation()}
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
{items.length > 0 ? (
|
|
167
|
+
<div className="space-y-0.5 max-h-32 overflow-y-auto pr-1">
|
|
168
|
+
{filteredItems.length > 0 ? (
|
|
169
|
+
filteredItems.map((item, idx) => renderItem(item, idx))
|
|
170
|
+
) : (
|
|
171
|
+
<p className="text-xs text-gray-400 italic">No matches</p>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
) : (
|
|
175
|
+
<p className="text-xs text-gray-300">{emptyText}</p>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Expanded content for service card
|
|
183
|
+
const ServiceExpandedContent = memo(
|
|
184
|
+
({ receives, sends, readsFrom, writesTo }: { receives: any[]; sends: any[]; readsFrom: any[]; writesTo: any[] }) => {
|
|
185
|
+
const hasMessages = receives.length > 0 || sends.length > 0;
|
|
186
|
+
const hasContainers = readsFrom.length > 0 || writesTo.length > 0;
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div className="border-t border-gray-100 px-4 py-3 space-y-4">
|
|
190
|
+
{/* Messages Row */}
|
|
191
|
+
{hasMessages && (
|
|
192
|
+
<div className="grid grid-cols-2 gap-x-6">
|
|
193
|
+
<SearchableBox
|
|
194
|
+
title="Receives"
|
|
195
|
+
icon={ArrowLongRightIcon}
|
|
196
|
+
iconColor="text-blue-400"
|
|
197
|
+
items={receives}
|
|
198
|
+
renderItem={(msg, idx) => {
|
|
199
|
+
const msgId = msg?.data?.id || msg?.id;
|
|
200
|
+
return msgId ? <MessageLink key={`${msgId}-${idx}`} message={msg} /> : null;
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
<SearchableBox
|
|
204
|
+
title="Sends"
|
|
205
|
+
icon={ArrowLongLeftIcon}
|
|
206
|
+
iconColor="text-emerald-400 rotate-180"
|
|
207
|
+
items={sends}
|
|
208
|
+
renderItem={(msg, idx) => {
|
|
209
|
+
const msgId = msg?.data?.id || msg?.id;
|
|
210
|
+
return msgId ? <MessageLink key={`${msgId}-${idx}`} message={msg} /> : null;
|
|
211
|
+
}}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{/* Data Row */}
|
|
217
|
+
{hasContainers && (
|
|
218
|
+
<div className="grid grid-cols-2 gap-x-6 pt-3 border-t border-gray-100">
|
|
219
|
+
<SearchableBox
|
|
220
|
+
title="Reads"
|
|
221
|
+
icon={CircleStackIcon}
|
|
222
|
+
iconColor="text-amber-400"
|
|
223
|
+
items={readsFrom}
|
|
224
|
+
renderItem={(container, idx) => {
|
|
225
|
+
const containerId = container?.data?.id || container?.id;
|
|
226
|
+
return containerId ? <ContainerLink key={`${containerId}-${idx}`} container={container} type="reads" /> : null;
|
|
227
|
+
}}
|
|
228
|
+
/>
|
|
229
|
+
<SearchableBox
|
|
230
|
+
title="Writes"
|
|
231
|
+
icon={CircleStackIcon}
|
|
232
|
+
iconColor="text-violet-400"
|
|
233
|
+
items={writesTo}
|
|
234
|
+
renderItem={(container, idx) => {
|
|
235
|
+
const containerId = container?.data?.id || container?.id;
|
|
236
|
+
return containerId ? <ContainerLink key={`${containerId}-${idx}`} container={container} type="writes" /> : null;
|
|
237
|
+
}}
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
|
|
97
246
|
const ServiceCard = memo(({ service }: { service: any }) => {
|
|
98
247
|
const data = service?.data || service;
|
|
248
|
+
const [isCollapsed, setIsCollapsed] = useState(true);
|
|
249
|
+
|
|
99
250
|
if (!data?.id) return null;
|
|
100
251
|
|
|
101
252
|
const receives = data.receives || [];
|
|
102
253
|
const sends = data.sends || [];
|
|
103
254
|
const readsFrom = data.readsFrom || [];
|
|
104
255
|
const writesTo = data.writesTo || [];
|
|
256
|
+
const specifications = getServiceSpecifications(data);
|
|
105
257
|
const hasMessages = receives.length > 0 || sends.length > 0;
|
|
106
258
|
const hasContainers = readsFrom.length > 0 || writesTo.length > 0;
|
|
259
|
+
const hasSpecs = specifications.length > 0;
|
|
260
|
+
const hasContent = hasMessages || hasContainers;
|
|
107
261
|
|
|
108
262
|
return (
|
|
109
|
-
<div className="bg-white border
|
|
263
|
+
<div className="bg-white border border-gray-200 rounded-xl shadow-sm hover:shadow-md transition-shadow">
|
|
110
264
|
{/* Service Header */}
|
|
111
|
-
<div
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
>
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
<span className="text-xs text-gray-500">v{data.version}</span>
|
|
119
|
-
</a>
|
|
120
|
-
<a
|
|
121
|
-
href={buildUrl(`/architecture/services/${data.id}/${data.version}`)}
|
|
122
|
-
className="p-1 hover:bg-pink-100 rounded-md transition-colors duration-200"
|
|
123
|
-
title="Expand service architecture"
|
|
124
|
-
onClick={(e) => e.stopPropagation()}
|
|
125
|
-
>
|
|
126
|
-
<ArrowsPointingOutIcon className="h-4 w-4 text-gray-500 hover:text-pink-600" />
|
|
127
|
-
</a>
|
|
128
|
-
</div>
|
|
129
|
-
|
|
130
|
-
{data.summary && <p className="mt-2 text-sm text-gray-600 line-clamp-2">{data.summary}</p>}
|
|
131
|
-
|
|
132
|
-
{/* Message Flow Diagram */}
|
|
133
|
-
{hasMessages && (
|
|
134
|
-
<div className="mt-4 flex items-stretch gap-3">
|
|
135
|
-
{/* Receives (Inbound) */}
|
|
136
|
-
<div className="flex-1 bg-blue-50 border border-blue-200 rounded-lg p-3">
|
|
137
|
-
<div className="flex items-center gap-1.5 mb-2">
|
|
138
|
-
<span className="text-xs font-semibold text-blue-700 uppercase">Inbound Messages</span>
|
|
139
|
-
<span className="text-xs text-blue-500">({receives.length})</span>
|
|
140
|
-
</div>
|
|
141
|
-
{receives.length > 0 ? (
|
|
142
|
-
<div className="space-y-1.5">
|
|
143
|
-
{receives.slice(0, 4).map((msg: any, idx: number) => {
|
|
144
|
-
const msgId = msg?.data?.id || msg?.id;
|
|
145
|
-
return msgId ? <MessageBadge key={`${msgId}-${idx}`} message={msg} /> : null;
|
|
146
|
-
})}
|
|
147
|
-
{receives.length > 4 && <p className="text-[10px] text-gray-500 text-center">+{receives.length - 4} more</p>}
|
|
148
|
-
</div>
|
|
149
|
-
) : (
|
|
150
|
-
<p className="text-[10px] text-gray-400 italic">No incoming messages</p>
|
|
151
|
-
)}
|
|
265
|
+
<div
|
|
266
|
+
onClick={() => hasContent && setIsCollapsed(!isCollapsed)}
|
|
267
|
+
className={`flex items-center justify-between px-4 py-3 ${hasContent ? 'cursor-pointer hover:bg-gray-50' : ''} transition-colors`}
|
|
268
|
+
>
|
|
269
|
+
<div className="flex items-center gap-2.5">
|
|
270
|
+
<div className="flex items-center justify-center w-8 h-8 bg-pink-100 rounded-lg">
|
|
271
|
+
<ServerIcon className="h-4 w-4 text-pink-600" />
|
|
152
272
|
</div>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
<ServerIcon className="h-6 w-6 text-pink-500" />
|
|
158
|
-
</div>
|
|
159
|
-
</div>
|
|
160
|
-
|
|
161
|
-
{/* Sends (Outbound) */}
|
|
162
|
-
<div className="flex-1 bg-green-50 border border-green-200 rounded-lg p-3">
|
|
163
|
-
<div className="flex items-center gap-1.5 mb-2">
|
|
164
|
-
<span className="text-xs font-semibold text-green-700 uppercase">Outbound Messages</span>
|
|
165
|
-
<span className="text-xs text-green-500">({sends.length})</span>
|
|
273
|
+
<div>
|
|
274
|
+
<div className="flex items-center gap-2">
|
|
275
|
+
<span className="font-semibold text-gray-900">{data.name || data.id}</span>
|
|
276
|
+
<span className="text-[11px] text-gray-500 font-medium bg-gray-100 px-1.5 py-0.5 rounded">v{data.version}</span>
|
|
166
277
|
</div>
|
|
167
|
-
{
|
|
168
|
-
<div className="space-y-1.5">
|
|
169
|
-
{sends.slice(0, 4).map((msg: any, idx: number) => {
|
|
170
|
-
const msgId = msg?.data?.id || msg?.id;
|
|
171
|
-
return msgId ? <MessageBadge key={`${msgId}-${idx}`} message={msg} /> : null;
|
|
172
|
-
})}
|
|
173
|
-
{sends.length > 4 && <p className="text-[10px] text-gray-500 text-center">+{sends.length - 4} more</p>}
|
|
174
|
-
</div>
|
|
175
|
-
) : (
|
|
176
|
-
<p className="text-[10px] text-gray-400 italic">No outgoing messages</p>
|
|
177
|
-
)}
|
|
278
|
+
{data.summary && <p className="text-xs text-gray-500 line-clamp-1 mt-0.5 max-w-md">{data.summary}</p>}
|
|
178
279
|
</div>
|
|
179
280
|
</div>
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
})}
|
|
197
|
-
{readsFrom.length > 3 && <span className="text-[10px] text-gray-500">+{readsFrom.length - 3} more</span>}
|
|
198
|
-
</div>
|
|
281
|
+
<div className="flex items-center gap-2">
|
|
282
|
+
{/* Specs in header - always visible */}
|
|
283
|
+
{hasSpecs && (
|
|
284
|
+
<div className="flex items-center gap-1.5">
|
|
285
|
+
{specifications.map((spec: any, idx: number) => (
|
|
286
|
+
<a
|
|
287
|
+
key={`${spec.type}-${idx}`}
|
|
288
|
+
href={getSpecUrl(spec, data.id, data.version)}
|
|
289
|
+
onClick={(e) => e.stopPropagation()}
|
|
290
|
+
className="flex items-center gap-1 px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs text-gray-600 hover:text-gray-900 transition-colors"
|
|
291
|
+
title={getSpecLabel(spec.type)}
|
|
292
|
+
>
|
|
293
|
+
<img src={buildUrl(`/icons/${getSpecIcon(spec.type)}.svg`, true)} alt="" className="h-3.5 w-3.5" />
|
|
294
|
+
<span className="hidden sm:inline">{getSpecLabel(spec.type)}</span>
|
|
295
|
+
</a>
|
|
296
|
+
))}
|
|
199
297
|
</div>
|
|
200
298
|
)}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
<div>
|
|
205
|
-
<div className="flex items-center gap-1.5 mb-2">
|
|
206
|
-
<CircleStackIcon className="h-3.5 w-3.5 text-purple-500" />
|
|
207
|
-
<span className="text-xs font-semibold text-gray-700">Writes to</span>
|
|
208
|
-
</div>
|
|
209
|
-
<div className="flex flex-wrap gap-1.5">
|
|
210
|
-
{writesTo.slice(0, 3).map((container: any, idx: number) => {
|
|
211
|
-
const containerId = container?.data?.id || container?.id;
|
|
212
|
-
return containerId ? (
|
|
213
|
-
<ContainerBadge key={`${containerId}-${idx}`} container={container} type="writes" />
|
|
214
|
-
) : null;
|
|
215
|
-
})}
|
|
216
|
-
{writesTo.length > 3 && <span className="text-[10px] text-gray-500">+{writesTo.length - 3} more</span>}
|
|
217
|
-
</div>
|
|
299
|
+
{hasContent && (
|
|
300
|
+
<div className="p-1.5 text-gray-400">
|
|
301
|
+
{isCollapsed ? <ChevronDownIcon className="h-4 w-4" /> : <ChevronUpIcon className="h-4 w-4" />}
|
|
218
302
|
</div>
|
|
219
303
|
)}
|
|
304
|
+
<a
|
|
305
|
+
href={buildUrl(`/architecture/services/${data.id}/${data.version}`)}
|
|
306
|
+
onClick={(e) => e.stopPropagation()}
|
|
307
|
+
className="p-1.5 text-gray-400 hover:text-pink-600 hover:bg-pink-50 rounded-lg transition-colors"
|
|
308
|
+
title="View service architecture"
|
|
309
|
+
>
|
|
310
|
+
<ArrowTopRightOnSquareIcon className="h-4 w-4" />
|
|
311
|
+
</a>
|
|
220
312
|
</div>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
{/* Expanded Content - Compact Flow */}
|
|
316
|
+
{!isCollapsed && hasContent && (
|
|
317
|
+
<ServiceExpandedContent receives={receives} sends={sends} readsFrom={readsFrom} writesTo={writesTo} />
|
|
221
318
|
)}
|
|
222
319
|
</div>
|
|
223
320
|
);
|
|
@@ -225,7 +322,7 @@ const ServiceCard = memo(({ service }: { service: any }) => {
|
|
|
225
322
|
|
|
226
323
|
const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
|
|
227
324
|
const data = subdomain?.data || subdomain;
|
|
228
|
-
const [isCollapsed, setIsCollapsed] = useState(
|
|
325
|
+
const [isCollapsed, setIsCollapsed] = useState(true);
|
|
229
326
|
|
|
230
327
|
if (!data?.id) return null;
|
|
231
328
|
|
|
@@ -233,39 +330,59 @@ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
|
|
|
233
330
|
const entities = data.entities || [];
|
|
234
331
|
|
|
235
332
|
return (
|
|
236
|
-
<div className="bg-
|
|
237
|
-
{/* Subdomain Header */}
|
|
238
|
-
<div
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
<
|
|
333
|
+
<div className="bg-white border border-gray-200 rounded-xl overflow-hidden shadow-sm">
|
|
334
|
+
{/* Subdomain Header - Clickable */}
|
|
335
|
+
<div
|
|
336
|
+
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
337
|
+
className={`flex items-center justify-between px-5 py-4 cursor-pointer hover:bg-gray-50 transition-colors ${!isCollapsed ? 'border-b border-gray-200' : ''}`}
|
|
338
|
+
>
|
|
339
|
+
<div className="flex items-center gap-3">
|
|
340
|
+
<div className="flex items-center justify-center w-9 h-9 bg-orange-100 rounded-lg">
|
|
341
|
+
<RectangleGroupIcon className="h-5 w-5 text-orange-600" />
|
|
342
|
+
</div>
|
|
343
|
+
<div>
|
|
344
|
+
<div className="flex items-center gap-2">
|
|
345
|
+
<h3 className="text-base font-semibold text-gray-900">{data.name || data.id}</h3>
|
|
346
|
+
<span className="text-[11px] text-gray-500 font-medium bg-white px-1.5 py-0.5 rounded border border-gray-200">
|
|
347
|
+
v{data.version}
|
|
348
|
+
</span>
|
|
349
|
+
{/* Show counts when collapsed */}
|
|
350
|
+
{isCollapsed && (services.length > 0 || entities.length > 0) && (
|
|
351
|
+
<span className="text-[11px] text-gray-400 ml-1">
|
|
352
|
+
{services.length > 0 && `${services.length} service${services.length > 1 ? 's' : ''}`}
|
|
353
|
+
{services.length > 0 && entities.length > 0 && ', '}
|
|
354
|
+
{entities.length > 0 && `${entities.length} entit${entities.length > 1 ? 'ies' : 'y'}`}
|
|
355
|
+
</span>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
<span className="text-[11px] text-gray-500 font-medium">Subdomain</span>
|
|
359
|
+
</div>
|
|
244
360
|
</div>
|
|
245
|
-
<div className="flex gap-
|
|
246
|
-
<
|
|
247
|
-
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
248
|
-
className="p-1 hover:bg-orange-200 rounded-md transition-colors cursor-pointer text-gray-500 hover:text-gray-700"
|
|
249
|
-
>
|
|
361
|
+
<div className="flex items-center gap-1">
|
|
362
|
+
<div className="p-2 text-gray-400">
|
|
250
363
|
{isCollapsed ? <ChevronDownIcon className="h-5 w-5" /> : <ChevronUpIcon className="h-5 w-5" />}
|
|
251
|
-
</
|
|
364
|
+
</div>
|
|
252
365
|
<a
|
|
253
366
|
href={buildUrl(`/architecture/domains/${data.id}/${data.version}`)}
|
|
254
|
-
className="p-1 hover:bg-orange-200 rounded-md transition-colors cursor-pointer text-gray-500 hover:text-gray-700"
|
|
255
|
-
title="Expand domain architecture"
|
|
256
367
|
onClick={(e) => e.stopPropagation()}
|
|
368
|
+
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-white rounded-lg transition-colors"
|
|
369
|
+
title="View subdomain architecture"
|
|
257
370
|
>
|
|
258
|
-
<
|
|
371
|
+
<ArrowTopRightOnSquareIcon className="h-5 w-5" />
|
|
259
372
|
</a>
|
|
260
373
|
</div>
|
|
261
374
|
</div>
|
|
262
375
|
|
|
263
376
|
{!isCollapsed && (
|
|
264
|
-
|
|
377
|
+
<div className="p-5 space-y-5">
|
|
265
378
|
{/* Subdomain Entities */}
|
|
266
379
|
{entities.length > 0 && (
|
|
267
|
-
<div
|
|
268
|
-
<
|
|
380
|
+
<div>
|
|
381
|
+
<div className="flex items-center gap-2 mb-3">
|
|
382
|
+
<BoxIcon className="h-4 w-4 text-purple-600" />
|
|
383
|
+
<h4 className="text-sm font-semibold text-gray-700">Entities</h4>
|
|
384
|
+
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded-full font-medium">{entities.length}</span>
|
|
385
|
+
</div>
|
|
269
386
|
<div className="flex flex-wrap gap-2">
|
|
270
387
|
{entities.map((entity: any) => {
|
|
271
388
|
const entityId = entity?.data?.id || entity?.id;
|
|
@@ -278,11 +395,14 @@ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
|
|
|
278
395
|
{/* Subdomain Services */}
|
|
279
396
|
{services.length > 0 && (
|
|
280
397
|
<div>
|
|
281
|
-
<
|
|
282
|
-
|
|
398
|
+
<div className="flex items-center gap-2 mb-3">
|
|
399
|
+
<ServerIcon className="h-4 w-4 text-pink-600" />
|
|
400
|
+
<h4 className="text-sm font-semibold text-gray-700">Services</h4>
|
|
401
|
+
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded-full font-medium">{services.length}</span>
|
|
402
|
+
</div>
|
|
403
|
+
<div className="space-y-3">
|
|
283
404
|
{services.map((service: any) => {
|
|
284
405
|
const serviceId = service?.data?.id || service?.id;
|
|
285
|
-
// Ensure we pass the service down with its messages populated
|
|
286
406
|
return serviceId ? <ServiceCard key={serviceId} service={service} /> : null;
|
|
287
407
|
})}
|
|
288
408
|
</div>
|
|
@@ -290,9 +410,9 @@ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
|
|
|
290
410
|
)}
|
|
291
411
|
|
|
292
412
|
{entities.length === 0 && services.length === 0 && (
|
|
293
|
-
<p className="text-sm text-gray-
|
|
413
|
+
<p className="text-sm text-gray-400 italic text-center py-4">No entities or services in this subdomain</p>
|
|
294
414
|
)}
|
|
295
|
-
|
|
415
|
+
</div>
|
|
296
416
|
)}
|
|
297
417
|
</div>
|
|
298
418
|
);
|
|
@@ -304,7 +424,7 @@ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
|
|
|
304
424
|
|
|
305
425
|
export default function DomainGrid({ domain }: DomainGridProps) {
|
|
306
426
|
const data = domain?.data;
|
|
307
|
-
if (!data) return <div>No domain data</div>;
|
|
427
|
+
if (!data) return <div className="text-gray-500">No domain data</div>;
|
|
308
428
|
|
|
309
429
|
const subdomains = data.domains || [];
|
|
310
430
|
const entities = data.entities || [];
|
|
@@ -332,39 +452,43 @@ export default function DomainGrid({ domain }: DomainGridProps) {
|
|
|
332
452
|
);
|
|
333
453
|
|
|
334
454
|
return (
|
|
335
|
-
<div className="
|
|
336
|
-
{/* Domain
|
|
337
|
-
<div className="
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
<
|
|
342
|
-
<h1 className="text-2xl font-bold text-gray-900">{data.name || data.id}</h1>
|
|
343
|
-
<span className="px-2 py-0.5 bg-yellow-200 text-yellow-800 text-xs font-medium rounded">v{data.version}</span>
|
|
344
|
-
<span className="px-2 py-0.5 bg-yellow-300 text-yellow-900 text-xs font-medium rounded">Domain</span>
|
|
455
|
+
<div className="w-full">
|
|
456
|
+
{/* Domain Header - Doc style */}
|
|
457
|
+
<div className="border-b border-gray-200 md:pb-4">
|
|
458
|
+
<div className="flex items-start justify-between">
|
|
459
|
+
<div>
|
|
460
|
+
<h2 className="text-2xl md:text-4xl font-bold text-black">{data.name || data.id}</h2>
|
|
461
|
+
{data.summary && <p className="text-lg pt-2 text-gray-500 font-light">{data.summary}</p>}
|
|
345
462
|
</div>
|
|
346
|
-
<div className="flex gap-
|
|
463
|
+
<div className="flex items-center gap-2 flex-shrink-0">
|
|
347
464
|
<a
|
|
348
465
|
href={buildUrl(`/docs/domains/${data.id}/${data.version}`)}
|
|
349
|
-
className="
|
|
466
|
+
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 hover:border-gray-300 transition-all"
|
|
350
467
|
>
|
|
351
468
|
View docs
|
|
469
|
+
<ArrowTopRightOnSquareIcon className="h-4 w-4 text-gray-400" />
|
|
352
470
|
</a>
|
|
353
471
|
<a
|
|
354
472
|
href={buildUrl(`/visualiser/domains/${data.id}/${data.version}`)}
|
|
355
|
-
className="
|
|
473
|
+
className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-white bg-gray-800 rounded-lg hover:bg-gray-900 transition-all"
|
|
356
474
|
>
|
|
357
475
|
Visualizer
|
|
476
|
+
<ArrowTopRightOnSquareIcon className="h-4 w-4 text-gray-400" />
|
|
358
477
|
</a>
|
|
359
478
|
</div>
|
|
360
479
|
</div>
|
|
480
|
+
</div>
|
|
361
481
|
|
|
362
|
-
|
|
363
|
-
|
|
482
|
+
{/* Domain Content */}
|
|
483
|
+
<div className="py-4 space-y-8">
|
|
364
484
|
{/* Domain Entities */}
|
|
365
485
|
{entities.length > 0 && (
|
|
366
|
-
<div
|
|
367
|
-
<
|
|
486
|
+
<div>
|
|
487
|
+
<div className="flex items-center gap-2 mb-4">
|
|
488
|
+
<BoxIcon className="h-5 w-5 text-purple-600" />
|
|
489
|
+
<h3 className="text-lg font-semibold text-gray-900">Entities</h3>
|
|
490
|
+
<span className="text-sm text-gray-500 bg-gray-100 px-2.5 py-0.5 rounded-full font-medium">{entities.length}</span>
|
|
491
|
+
</div>
|
|
368
492
|
<div className="flex flex-wrap gap-2">
|
|
369
493
|
{entities.map((entity: any) => {
|
|
370
494
|
const entityId = entity?.data?.id || entity?.id;
|
|
@@ -374,11 +498,17 @@ export default function DomainGrid({ domain }: DomainGridProps) {
|
|
|
374
498
|
</div>
|
|
375
499
|
)}
|
|
376
500
|
|
|
377
|
-
{/* Top-level Services
|
|
501
|
+
{/* Top-level Services */}
|
|
378
502
|
{topLevelServices.length > 0 && (
|
|
379
|
-
<div
|
|
380
|
-
<
|
|
381
|
-
|
|
503
|
+
<div>
|
|
504
|
+
<div className="flex items-center gap-2 mb-4">
|
|
505
|
+
<ServerIcon className="h-5 w-5 text-pink-600" />
|
|
506
|
+
<h3 className="text-lg font-semibold text-gray-900">Services</h3>
|
|
507
|
+
<span className="text-sm text-gray-500 bg-gray-100 px-2.5 py-0.5 rounded-full font-medium">
|
|
508
|
+
{topLevelServices.length}
|
|
509
|
+
</span>
|
|
510
|
+
</div>
|
|
511
|
+
<div className="space-y-3">
|
|
382
512
|
{topLevelServices.map((service: any) => {
|
|
383
513
|
const serviceId = service?.data?.id || service?.id;
|
|
384
514
|
return serviceId ? <ServiceCard key={serviceId} service={service} /> : null;
|
|
@@ -387,10 +517,16 @@ export default function DomainGrid({ domain }: DomainGridProps) {
|
|
|
387
517
|
</div>
|
|
388
518
|
)}
|
|
389
519
|
|
|
390
|
-
{/* Subdomains
|
|
520
|
+
{/* Subdomains */}
|
|
391
521
|
{subdomains.length > 0 && (
|
|
392
522
|
<div>
|
|
393
|
-
<
|
|
523
|
+
<div className="flex items-center gap-2 mb-4">
|
|
524
|
+
<RectangleGroupIcon className="h-5 w-5 text-orange-600" />
|
|
525
|
+
<h3 className="text-lg font-semibold text-gray-900">Subdomains</h3>
|
|
526
|
+
<span className="text-sm text-gray-500 bg-gray-100 px-2.5 py-0.5 rounded-full font-medium">
|
|
527
|
+
{subdomains.length}
|
|
528
|
+
</span>
|
|
529
|
+
</div>
|
|
394
530
|
<div className="space-y-4">
|
|
395
531
|
{subdomains.map((subdomain: any) => {
|
|
396
532
|
const subdomainId = subdomain?.data?.id || subdomain?.id;
|
|
@@ -402,7 +538,10 @@ export default function DomainGrid({ domain }: DomainGridProps) {
|
|
|
402
538
|
|
|
403
539
|
{/* Empty state */}
|
|
404
540
|
{entities.length === 0 && services.length === 0 && subdomains.length === 0 && (
|
|
405
|
-
<div className="text-center py-
|
|
541
|
+
<div className="text-center py-12">
|
|
542
|
+
<div className="flex items-center justify-center w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-2xl">
|
|
543
|
+
<RectangleGroupIcon className="h-8 w-8 text-gray-400" />
|
|
544
|
+
</div>
|
|
406
545
|
<p className="text-gray-500">This domain has no entities, services, or subdomains defined.</p>
|
|
407
546
|
</div>
|
|
408
547
|
)}
|