From f2e8111873489da4813271db0102747fefb63297 Mon Sep 17 00:00:00 2001 From: David Boyne Date: Tue, 9 Dec 2025 16:59:31 +0000 Subject: [PATCH 1/4] EventCatalog V3 (#1864) * backup * backup * backup * backup * backup * backup * backup * backup * navigation only shows resources that are in teh catalog * navigation only shows resources that are in teh catalog * added favourites * updated the default landing page * removed unused code * turnoff pagefind * turnoff pagefind * turnoff pagefind * Create late-zoos-scream.md Fix 'Get Started' link in README (#1863) Updated the 'Get Started' link in the README. Version Packages (beta) (#1868) Co-authored-by: github-actions[bot] chore(core): fixing circular dep in JS (#1870) * chore(core): fixing circular dep in JS * Create selfish-geese-wave.md fix(core): fixed issues with nested sidebar state (#1873) * fix(core): fixed issues with nested sidebar state * Create mighty-walls-watch.md fix(core): fixed issues with nested sidebar state chore(core): updated styles for v3 (#1874) * chore(core): updated styles for v3 * Create khaki-humans-wonder.md Version Packages (beta) (#1872) Co-authored-by: github-actions[bot] feat(core): visualizer now has presentation mode (#1875) * feat(core): visualizer now has presentation mode * Create clever-turkeys-wink.md Version Packages (beta) (#1876) Co-authored-by: github-actions[bot] chore(core): updated cli logger (#1877) * chore(core): updated cli logger * Create sweet-feet-reflect.md Version Packages (beta) (#1878) Co-authored-by: github-actions[bot] feat(core): added channel support in nav and search (#1879) * chore(core): updated cli logger * feat(core): added channel support in nav and search * Create gold-dryers-sleep.md Version Packages (beta) (#1880) Co-authored-by: github-actions[bot] chore(core): updated react-syntax-highlighter (#1881) * chore(core): updated react-syntax-highlighter * Create tiny-suns-lie.md Version Packages (beta) (#1882) Co-authored-by: github-actions[bot] chore(core): updated logger for the ecstudio watcher (#1884) * chore(core): updated logger for the ecstudio watcher * Create five-cougars-smell.md chore(core): added empty state to nested sidebar (#1886) * chore(core): updated logger for the ecstudio watcher * chore(core): added empty state to nestedsidebar * Create light-humans-mate.md Version Packages (beta) (#1885) Co-authored-by: github-actions[bot] fix(core): fixed issue embedding pages on build fix(core): fixed issue embedding pages on build (#1887) * fix(core): fixed issue embedding pages on build * Create eleven-terms-agree.md Version Packages (beta) (#1888) Co-authored-by: github-actions[bot] fix(core): fixed deployment of example catalogs on releases (#1889) * fix(core): fixed issue embedding pages on build * fix(core): fixed issue embedding pages on build * Create early-bottles-reply.md Version Packages (beta) (#1890) Co-authored-by: github-actions[bot] fix(core): fixed issue embedding pages on build (#1891) * fix(core): fixed issue embedding pages on build * fix(core): fixed issue embedding pages on build * fix(core): fixed issue embedding pages on build * Create dirty-sheep-doubt.md Version Packages (beta) (#1892) Co-authored-by: github-actions[bot] fix(core): fixed issue embedding pages on build (#1893) * fix(core): fixed issue embedding pages on build * fix(core): fixed issue embedding pages on build * fix(core): fixed issue embedding pages on build * fix(core): fixed issue embedding pages on build * Create curvy-clocks-complain.md Version Packages (beta) (#1894) Co-authored-by: github-actions[bot] feat(core): added support for titles on admonitions (#1895) * feat(core): added support for titles on admonitions * Create cyan-readers-hear.md feat(core): updated homepage styles (#1897) * feat(core): updated homepage styles * Create tall-adults-end.md Version Packages (beta) (#1898) Co-authored-by: github-actions[bot] chore(core): removed unused icons on domain grid (#1899) * chore(core): removed unused icons on domain grid * Create good-humans-love.md Version Packages (beta) (#1900) Co-authored-by: github-actions[bot] fix(core): mdx pages are added to teams and users (#1901) * fix(core): mdx pages are added to teams and users * Create sharp-files-occur.md chore(core): auth is now more explict opt in (#1902) * chore(core): auth is now more explict opt in * Create happy-gorillas-fold.md Version Packages (beta) (#1903) Co-authored-by: github-actions[bot] chore(core): auth is now more explict opt in (#1904) * chore(core): auth is now more explict opt in * fix(core): fixed schema explorer in SSR mode * Create shaggy-adults-promise.md fix(core): problems with asyncapi loading in the DOM (#1906) * fix(core): problems with asyncapi loading in the DOM * Create warm-scissors-help.md Version Packages (beta) (#1905) Co-authored-by: github-actions[bot] fix(core): fixed accessibility issues (#1907) * fix(core): fixed accessibility issues * Create thin-lizards-guess.md Version Packages (beta) (#1908) Co-authored-by: github-actions[bot] chore(core): added icons to spec files in sidebar (#1909) * chore(core): added icons to spec files in sidebar * Create beige-nails-wonder.md * chore(core): added icons to spec files in sidebar Version Packages (beta) (#1910) Co-authored-by: github-actions[bot] feat(core): embedding visualizer can be embedded with animations (#1911) * feat(core): embedding visualizer can be embedded with animations * Create thirty-shrimps-argue.md Version Packages (beta) (#1912) Co-authored-by: github-actions[bot] chore(core): added release automation for SaaS example chore(core): added v3 beta message update (#1913) * chore(core): added v3 beta message update * Create plenty-crabs-cheat.md Version Packages (beta) (#1914) Co-authored-by: github-actions[bot] update readme chore(core): updated packages (#1916) * chore(core): updated packages * Create rotten-lamps-vanish.md feat(core): simplified eventcatalog chat (#1919) * feat(core): simplified eventcatalog chat * feat(core): simplified eventcatalog chat * Create fresh-yaks-yawn.md Version Packages (beta) (#1918) Co-authored-by: github-actions[bot] chore(core): refactored features into astro custom integrations (#1920) * chore(core): refactored features into astro custom integrations * chore(core): refactored features into astro custom integrations * Create lemon-pugs-sing.md Version Packages (beta) (#1921) Co-authored-by: github-actions[bot] chore(core): removed some redundant files (#1922) * chore(core): removed some redundant files * Create khaki-scissors-boil.md * chore(core): removed some redundant files Version Packages (beta) (#1923) Co-authored-by: github-actions[bot] chore(core): removed some redundant files (#1924) * chore(core): removed some redundant files * Create light-meals-play.md Version Packages (beta) (#1925) Co-authored-by: github-actions[bot] feat(core): added domain integration map and attachments to sidebar (#1931) * fix(core): removed duplicated edge labels * feat(core): added domain integration map and attachments to sidebar * Create tame-tools-protect.md Version Packages (beta) (#1932) Co-authored-by: github-actions[bot] Repoint fork to publish to npm in github package registry Added new schema reference package Fix linting issues Added package publishing to workflow permission Update npx build command Attempting to use resolve, rather than dereference Revert back to dereference and ignore circular New FHIR Viewer Updated and introduced new FHIRViewer Revert JSON Schema changes Added flexible sidebar Sync pnpm lock file --- .astro/content-assets.mjs | 1 - .astro/content-modules.mjs | 1 - .astro/content.d.ts | 199 -- .astro/data-store.json | 10 +- .astro/settings.json | 8 + .astro/types.d.ts | 1 - .changeset/beige-nails-wonder.md | 5 + .changeset/clever-turkeys-wink.md | 5 + .changeset/curvy-clocks-complain.md | 5 + .changeset/cyan-readers-hear.md | 5 + .changeset/dirty-sheep-doubt.md | 5 + .changeset/early-bottles-reply.md | 5 + .changeset/eleven-terms-agree.md | 5 + .changeset/five-cougars-smell.md | 5 + .changeset/fresh-yaks-yawn.md | 5 + .changeset/gold-dryers-sleep.md | 5 + .changeset/good-humans-love.md | 5 + .changeset/happy-gorillas-fold.md | 5 + .changeset/khaki-humans-wonder.md | 5 + .changeset/khaki-scissors-boil.md | 5 + .changeset/late-zoos-scream.md | 5 + .changeset/lemon-pugs-sing.md | 5 + .changeset/light-humans-mate.md | 5 + .changeset/light-meals-play.md | 5 + .changeset/mighty-walls-watch.md | 5 + .changeset/plenty-crabs-cheat.md | 5 + .changeset/pre.json | 40 + .changeset/rotten-lamps-vanish.md | 5 + .changeset/selfish-geese-wave.md | 5 + .changeset/shaggy-adults-promise.md | 5 + .changeset/sharp-files-occur.md | 5 + .changeset/sweet-feet-reflect.md | 5 + .changeset/tall-adults-end.md | 5 + .changeset/tame-tools-protect.md | 5 + .changeset/thin-lizards-guess.md | 5 + .changeset/thirty-shrimps-argue.md | 5 + .changeset/tiny-suns-lie.md | 5 + .changeset/warm-scissors-help.md | 5 + .../redeploy-eventcatalog-examples.yml | 47 + .gitignore | 6 + .npmrc | 6 + CHANGELOG.md | 151 ++ Dockerfile.server | 2 +- README.md | 139 +- default-files-for-collections/changelogs.md | 5 - default-files-for-collections/channels.md | 8 - default-files-for-collections/commands.md | 8 - default-files-for-collections/domains.md | 8 - default-files-for-collections/events.md | 8 - default-files-for-collections/flows.md | 11 - default-files-for-collections/queries.md | 8 - default-files-for-collections/services.md | 8 - .../ubiquitousLanguages.md | 7 - eventcatalog/astro.config.mjs | 5 +- .../integrations/ecstudio-watcher.mjs | 2 +- .../integrations/eventcatalog-features.ts | 69 + eventcatalog/public/icons/asyncapi-black.svg | 2 + eventcatalog/public/icons/graphql-black.svg | 1 + eventcatalog/public/icons/openapi-black.svg | 1 + eventcatalog/public/logo_old.png | Bin 54304 -> 0 bytes .../src/__tests__/middleware-auth.spec.ts | 2 +- .../src/components/ChatPanel/ChatPanel.tsx | 821 ++++++ .../components/ChatPanel/ChatPanelButton.tsx | 24 + .../src/components/CopyAsMarkdown.tsx | 20 +- .../src/components/DiscoverInsight.astro | 61 - .../src/components/FavoriteButton.tsx | 54 + .../src/components/Grids/DomainGrid.tsx | 742 +++--- .../src/components/Grids/MessageGrid.tsx | 691 ++---- .../src/components/Grids/ServiceGrid.tsx | 540 ---- eventcatalog/src/components/Header.astro | 91 +- .../Lists/CustomSideBarSectionList.astro | 55 - .../src/components/Lists/ProtocolList.tsx | 74 - .../src/components/Lists/RepositoryList.astro | 37 - .../components/Lists/SpecificationsList.astro | 67 - .../src/components/Lists/VersionList.astro | 4 +- .../components/MDX/NodeGraph/NodeGraph.tsx | 65 +- .../SchemaExplorer/FHIRSchemaViewer.tsx | 8 +- .../SchemaExplorer/SchemaDetailsPanel.tsx | 10 +- .../SchemaExplorer/SchemaExplorer.tsx | 10 - .../SchemaExplorer/SchemaPageViewer.tsx | 37 + .../src/components/Search/Search.astro | 82 +- .../src/components/Search/SearchModal.tsx | 1104 +++------ .../components/SideBars/ChannelSideBar.astro | 204 -- .../SideBars/ContainerSideBar.astro | 183 -- .../components/SideBars/DomainSideBar.astro | 277 --- .../components/SideBars/EntitySideBar.astro | 139 -- .../src/components/SideBars/FlowSideBar.astro | 132 - .../components/SideBars/MessageSideBar.astro | 251 -- .../components/SideBars/ServiceSideBar.astro | 298 --- .../components/CollapsibleGroup.tsx | 46 - .../components/MessageList.tsx | 78 - .../components/SpecificationList.tsx | 83 - .../SideNav/ListViewSideBar/index.tsx | 1250 ---------- .../SideNav/ListViewSideBar/types.ts | 91 - .../SideNav/ListViewSideBar/utils.ts | 201 -- .../SideNav/NestedSideBar/SearchBar.tsx | 305 +++ .../SideNav/NestedSideBar/__tests__/mocks.ts | 247 ++ .../__tests__/sidebar-builder.spec.ts | 2194 +++++++++++++++++ .../NestedSideBar/builders/container.ts | 75 + .../SideNav/NestedSideBar/builders/domain.ts | 111 + .../SideNav/NestedSideBar/builders/flow.ts | 29 + .../SideNav/NestedSideBar/builders/message.ts | 93 + .../SideNav/NestedSideBar/builders/service.ts | 154 ++ .../SideNav/NestedSideBar/builders/shared.ts | 160 ++ .../SideNav/NestedSideBar/index.tsx | 1102 +++++++++ .../SideNav/NestedSideBar/sidebar-builder.ts | 411 +++ .../SideNav/NestedSideBar/storage.ts | 90 + .../src/components/SideNav/SideNav.astro | 81 +- .../SideNav/TreeView/getTreeView.ts | 190 -- .../src/components/SideNav/TreeView/index.tsx | 94 - .../src/components/TreeView/index.tsx | 328 --- .../src/components/TreeView/styles.module.css | 264 -- .../src/components/TreeView/useSlots.ts | 95 - eventcatalog/src/content.config.ts | 16 +- eventcatalog/src/enterprise/ai/chat-api.ts | 262 ++ eventcatalog/src/enterprise/auth/[...auth].ts | 3 + .../{pages => enterprise}/auth/error.astro | 0 eventcatalog/src/enterprise/auth/login.astro | 420 ++++ .../auth/middleware}/middleware-auth.ts | 0 .../auth/middleware}/middleware.ts | 0 .../auth/unauthorized.astro} | 2 +- .../enterprise/collections/chat-prompts.ts | 32 - .../src/enterprise/collections/index.ts | 1 - .../pages/docs/custom/index.astro | 2 +- .../__tests__/utils/chat-prompts.spec.ts | 94 - .../eventcatalog-chat/components/Chat.tsx | 60 - .../components/ChatMessage.tsx | 414 ---- .../components/ChatSidebar.tsx | 169 -- .../components/InputModal.tsx | 244 -- .../components/MentionInput.tsx | 211 -- .../components/WelcomePromptArea.tsx | 176 -- .../components/default-prompts.ts | 93 - .../components/hooks/ChatProvider.tsx | 143 -- .../components/windows/ChatWindow.server.tsx | 387 --- .../eventcatalog-chat/pages/api/chat.ts | 59 - .../eventcatalog-chat/pages/chat/index.astro | 104 - .../providers/ai-provider.ts | 140 -- .../eventcatalog-chat/providers/anthropic.ts | 28 - .../eventcatalog-chat/providers/google.ts | 41 - .../eventcatalog-chat/providers/index.ts | 26 - .../eventcatalog-chat/providers/openai.ts | 61 - .../eventcatalog-chat/utils/chat-prompts.ts | 50 - .../{pages => enterprise}/plans/index.astro | 2 +- .../src/layouts/DirectoryLayout.astro | 6 +- eventcatalog/src/layouts/DiscoverLayout.astro | 8 +- eventcatalog/src/layouts/Footer.astro | 13 +- .../src/layouts/VerticalSideBarLayout.astro | 299 +-- .../src/layouts/VisualiserLayout.astro | 6 +- eventcatalog/src/pages/_index.astro | 645 ++++- .../[type]/[id]/[version]/_index.data.ts | 64 + .../[type]/[id]/[version]/index.astro | 29 + .../src/pages/architecture/[type]/index.astro | 14 - .../src/pages/architecture/architecture.astro | 110 - .../architecture/docs/[type]/index.astro | 14 - eventcatalog/src/pages/auth/login.astro | 280 --- eventcatalog/src/pages/chat/feature.astro | 179 -- eventcatalog/src/pages/chat/index.astro | 10 - .../src/pages/directory/[type]/_index.data.ts | 8 +- .../pages/docs/[type]/[id]/[version].md.ts | 2 +- .../docs/[type]/[id]/[version]/_index.data.ts | 5 +- .../[id]/[version]/asyncapi/[filename].astro | 22 +- .../[id]/[version]/changelog/_index.data.ts | 6 +- .../[id]/[version]/changelog/index.astro | 20 +- .../[id]/[version]/graphql/[filename].astro | 2 +- .../docs/[type]/[id]/[version]/index.astro | 550 +++-- .../src/pages/docs/[type]/[id]/index.astro | 8 +- .../docs/[type]/[id]/language/_index.data.ts | 5 +- .../docs/[type]/[id]/language/index.astro | 30 +- eventcatalog/src/pages/docs/_default-docs.mdx | 25 - .../src/pages/docs/custom/feature.astro | 2 +- .../src/pages/docs/custom/index.astro | 2 +- eventcatalog/src/pages/docs/index.astro | 33 - eventcatalog/src/pages/docs/teams/[id].mdx.ts | 36 + .../src/pages/docs/teams/[id]/_index.data.ts | 4 +- eventcatalog/src/pages/docs/users/[id].mdx.ts | 36 + .../src/pages/docs/users/[id]/_index.data.ts | 4 +- .../[type]/[id]/[version]/_index.data.ts | 77 + .../schemas/[type]/[id]/[version]/index.astro | 90 + .../src/pages/schemas/explorer/_index.data.ts | 178 ++ .../src/pages/schemas/explorer/index.astro | 25 + eventcatalog/src/pages/schemas/index.astro | 175 -- eventcatalog/src/pages/studio.astro | 8 +- .../pages/visualiser/[type]/[id]/index.astro | 4 +- eventcatalog/src/remark-plugins/directives.ts | 39 +- eventcatalog/src/stores/favorites-store.ts | 83 + eventcatalog/src/stores/sidebar-store.ts | 8 + .../utils/__tests__/channels/channels.spec.ts | 2 +- .../__tests__/collections/file-diffs.spec.ts | 2 +- .../utils/__tests__/commands/commands.spec.ts | 2 +- .../__tests__/domains/node-graph.spec.ts | 164 +- .../utils/__tests__/entities/entities.spec.ts | 2 +- .../src/utils/__tests__/events/events.spec.ts | 2 +- .../src/utils/__tests__/features.spec.ts | 43 +- .../utils/__tests__/messages/messages.spec.ts | 2 +- .../utils/__tests__/queries/queries.spec.ts | 2 +- .../utils/__tests__/services/services.spec.ts | 2 + .../src/utils/collections/changelogs.ts | 11 +- .../src/utils/{ => collections}/channels.ts | 114 +- .../src/utils/collections/commands.ts | 132 + .../src/utils/collections/containers.ts | 79 +- eventcatalog/src/utils/collections/domains.ts | 270 +- .../src/utils/collections/entities.ts | 114 + eventcatalog/src/utils/collections/events.ts | 134 + eventcatalog/src/utils/collections/flows.ts | 86 +- .../src/utils/{ => collections}/messages.ts | 17 +- eventcatalog/src/utils/collections/queries.ts | 127 + eventcatalog/src/utils/collections/schemas.ts | 38 +- .../src/utils/collections/services.ts | 170 +- eventcatalog/src/utils/collections/teams.ts | 94 + eventcatalog/src/utils/collections/types.ts | 6 + eventcatalog/src/utils/collections/users.ts | 122 + eventcatalog/src/utils/collections/util.ts | 58 +- eventcatalog/src/utils/commands.ts | 112 - eventcatalog/src/utils/entities.ts | 96 - eventcatalog/src/utils/events.ts | 108 - eventcatalog/src/utils/feature.ts | 18 +- .../src/utils/{collections => }/file-diffs.ts | 2 +- eventcatalog/src/utils/generators/index.ts | 10 - .../utils/node-graphs/container-node-graph.ts | 2 + .../utils/node-graphs/domain-entity-map.ts | 22 +- .../src/utils/node-graphs/domains-canvas.ts | 24 +- .../utils/node-graphs/domains-node-graph.ts | 100 +- .../src/utils/node-graphs/flows-node-graph.ts | 42 +- .../utils/node-graphs/message-node-graph.ts | 85 +- .../utils/node-graphs/services-node-graph.ts | 42 +- .../utils/page-loaders/page-data-loader.ts | 8 +- eventcatalog/src/utils/queries.ts | 108 - eventcatalog/src/utils/resource-files.ts | 86 + eventcatalog/src/utils/teams.ts | 72 - eventcatalog/src/utils/users.ts | 72 - eventcatalog/tailwind.config.mjs | 14 + eventcatalog/tsconfig.json | 6 +- examples/default/domains/E-Commerce/index.mdx | 39 +- .../E-Commerce/subdomains/Orders/index.mdx | 12 - .../containers/inventory-db/index.mdx | 2 +- .../Orders/services/OrdersService/index.mdx | 1 + .../E-Commerce/subdomains/Payment/index.mdx | 2 + .../containers/payment-cache/index.mdx | 2 +- .../flows/CancelSubscription/index.mdx | 2 +- .../flows/SubscriptionRenewed/index.mdx | 2 +- .../subdomains/Subscriptions/index.mdx | 4 +- .../services/BillingService/index.mdx | 3 + examples/default/eventcatalog.auth.js | 30 +- examples/default/eventcatalog.config.js | 29 +- .../pages/{homepage.astro => _homepage.astro} | 0 images/example.png | Bin 185513 -> 790900 bytes images/sponsors/gravitee-logo-black.svg | 54 - images/sponsors/gravitee-logo-white.webp | Bin 2314 -> 0 bytes images/sponsors/hookdeck.svg | 11 - images/sponsors/oso-logo-green.png | Bin 34558 -> 0 bytes package.json | 32 +- pnpm-lock.yaml | 907 +++---- scripts/build-ci.js | 1 + scripts/catalog-generator/README.md | 16 - .../generate-large-catalog.js | 102 - .../specs/DOMAIN/APP/1.0.0/APP.yaml | 133 - .../{pagefind.js => start-server-locally.js} | 14 +- {__mocks__ => src/__mocks__}/astro-content.ts | 0 src/eventcatalog.config.ts | 15 + src/eventcatalog.ts | 148 +- src/generate.js | 6 +- src/utils/cli-logger.ts | 40 + vitest.config.js | 2 +- 263 files changed, 12482 insertions(+), 13127 deletions(-) delete mode 100644 .astro/content-assets.mjs delete mode 100644 .astro/content-modules.mjs delete mode 100644 .astro/content.d.ts delete mode 100644 .astro/types.d.ts create mode 100644 .changeset/beige-nails-wonder.md create mode 100644 .changeset/clever-turkeys-wink.md create mode 100644 .changeset/curvy-clocks-complain.md create mode 100644 .changeset/cyan-readers-hear.md create mode 100644 .changeset/dirty-sheep-doubt.md create mode 100644 .changeset/early-bottles-reply.md create mode 100644 .changeset/eleven-terms-agree.md create mode 100644 .changeset/five-cougars-smell.md create mode 100644 .changeset/fresh-yaks-yawn.md create mode 100644 .changeset/gold-dryers-sleep.md create mode 100644 .changeset/good-humans-love.md create mode 100644 .changeset/happy-gorillas-fold.md create mode 100644 .changeset/khaki-humans-wonder.md create mode 100644 .changeset/khaki-scissors-boil.md create mode 100644 .changeset/late-zoos-scream.md create mode 100644 .changeset/lemon-pugs-sing.md create mode 100644 .changeset/light-humans-mate.md create mode 100644 .changeset/light-meals-play.md create mode 100644 .changeset/mighty-walls-watch.md create mode 100644 .changeset/plenty-crabs-cheat.md create mode 100644 .changeset/pre.json create mode 100644 .changeset/rotten-lamps-vanish.md create mode 100644 .changeset/selfish-geese-wave.md create mode 100644 .changeset/shaggy-adults-promise.md create mode 100644 .changeset/sharp-files-occur.md create mode 100644 .changeset/sweet-feet-reflect.md create mode 100644 .changeset/tall-adults-end.md create mode 100644 .changeset/tame-tools-protect.md create mode 100644 .changeset/thin-lizards-guess.md create mode 100644 .changeset/thirty-shrimps-argue.md create mode 100644 .changeset/tiny-suns-lie.md create mode 100644 .changeset/warm-scissors-help.md create mode 100644 .github/workflows/redeploy-eventcatalog-examples.yml delete mode 100644 default-files-for-collections/changelogs.md delete mode 100644 default-files-for-collections/channels.md delete mode 100644 default-files-for-collections/commands.md delete mode 100644 default-files-for-collections/domains.md delete mode 100644 default-files-for-collections/events.md delete mode 100644 default-files-for-collections/flows.md delete mode 100644 default-files-for-collections/queries.md delete mode 100644 default-files-for-collections/services.md delete mode 100644 default-files-for-collections/ubiquitousLanguages.md create mode 100644 eventcatalog/integrations/eventcatalog-features.ts create mode 100644 eventcatalog/public/icons/asyncapi-black.svg create mode 100644 eventcatalog/public/icons/graphql-black.svg create mode 100644 eventcatalog/public/icons/openapi-black.svg delete mode 100644 eventcatalog/public/logo_old.png create mode 100644 eventcatalog/src/components/ChatPanel/ChatPanel.tsx create mode 100644 eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx delete mode 100644 eventcatalog/src/components/DiscoverInsight.astro create mode 100644 eventcatalog/src/components/FavoriteButton.tsx delete mode 100644 eventcatalog/src/components/Grids/ServiceGrid.tsx delete mode 100644 eventcatalog/src/components/Lists/CustomSideBarSectionList.astro delete mode 100644 eventcatalog/src/components/Lists/ProtocolList.tsx delete mode 100644 eventcatalog/src/components/Lists/RepositoryList.astro delete mode 100644 eventcatalog/src/components/Lists/SpecificationsList.astro create mode 100644 eventcatalog/src/components/SchemaExplorer/SchemaPageViewer.tsx delete mode 100644 eventcatalog/src/components/SideBars/ChannelSideBar.astro delete mode 100644 eventcatalog/src/components/SideBars/ContainerSideBar.astro delete mode 100644 eventcatalog/src/components/SideBars/DomainSideBar.astro delete mode 100644 eventcatalog/src/components/SideBars/EntitySideBar.astro delete mode 100644 eventcatalog/src/components/SideBars/FlowSideBar.astro delete mode 100644 eventcatalog/src/components/SideBars/MessageSideBar.astro delete mode 100644 eventcatalog/src/components/SideBars/ServiceSideBar.astro delete mode 100644 eventcatalog/src/components/SideNav/ListViewSideBar/components/CollapsibleGroup.tsx delete mode 100644 eventcatalog/src/components/SideNav/ListViewSideBar/components/MessageList.tsx delete mode 100644 eventcatalog/src/components/SideNav/ListViewSideBar/components/SpecificationList.tsx delete mode 100644 eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx delete mode 100644 eventcatalog/src/components/SideNav/ListViewSideBar/types.ts delete mode 100644 eventcatalog/src/components/SideNav/ListViewSideBar/utils.ts create mode 100644 eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx create mode 100644 eventcatalog/src/components/SideNav/NestedSideBar/__tests__/mocks.ts create mode 100644 eventcatalog/src/components/SideNav/NestedSideBar/__tests__/sidebar-builder.spec.ts create mode 100644 eventcatalog/src/components/SideNav/NestedSideBar/builders/container.ts create mode 100644 eventcatalog/src/components/SideNav/NestedSideBar/builders/domain.ts create mode 100644 eventcatalog/src/components/SideNav/NestedSideBar/builders/flow.ts create mode 100644 eventcatalog/src/components/SideNav/NestedSideBar/builders/message.ts create mode 100644 eventcatalog/src/components/SideNav/NestedSideBar/builders/service.ts create mode 100644 eventcatalog/src/components/SideNav/NestedSideBar/builders/shared.ts create mode 100644 eventcatalog/src/components/SideNav/NestedSideBar/index.tsx create mode 100644 eventcatalog/src/components/SideNav/NestedSideBar/sidebar-builder.ts create mode 100644 eventcatalog/src/components/SideNav/NestedSideBar/storage.ts delete mode 100644 eventcatalog/src/components/SideNav/TreeView/getTreeView.ts delete mode 100644 eventcatalog/src/components/SideNav/TreeView/index.tsx delete mode 100644 eventcatalog/src/components/TreeView/index.tsx delete mode 100644 eventcatalog/src/components/TreeView/styles.module.css delete mode 100644 eventcatalog/src/components/TreeView/useSlots.ts create mode 100644 eventcatalog/src/enterprise/ai/chat-api.ts create mode 100644 eventcatalog/src/enterprise/auth/[...auth].ts rename eventcatalog/src/{pages => enterprise}/auth/error.astro (100%) create mode 100644 eventcatalog/src/enterprise/auth/login.astro rename eventcatalog/src/{ => enterprise/auth/middleware}/middleware-auth.ts (100%) rename eventcatalog/src/{ => enterprise/auth/middleware}/middleware.ts (100%) rename eventcatalog/src/{pages/unauthorized/index.astro => enterprise/auth/unauthorized.astro} (97%) delete mode 100644 eventcatalog/src/enterprise/collections/chat-prompts.ts delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/__tests__/utils/chat-prompts.spec.ts delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/components/Chat.tsx delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/components/ChatMessage.tsx delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/components/ChatSidebar.tsx delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/components/InputModal.tsx delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/components/MentionInput.tsx delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/components/WelcomePromptArea.tsx delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/components/default-prompts.ts delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/components/hooks/ChatProvider.tsx delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/components/windows/ChatWindow.server.tsx delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/pages/api/chat.ts delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/pages/chat/index.astro delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/providers/ai-provider.ts delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/providers/anthropic.ts delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/providers/google.ts delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/providers/index.ts delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/providers/openai.ts delete mode 100644 eventcatalog/src/enterprise/eventcatalog-chat/utils/chat-prompts.ts rename eventcatalog/src/{pages => enterprise}/plans/index.astro (99%) create mode 100644 eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts create mode 100644 eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro delete mode 100644 eventcatalog/src/pages/architecture/[type]/index.astro delete mode 100644 eventcatalog/src/pages/architecture/architecture.astro delete mode 100644 eventcatalog/src/pages/architecture/docs/[type]/index.astro delete mode 100644 eventcatalog/src/pages/auth/login.astro delete mode 100644 eventcatalog/src/pages/chat/feature.astro delete mode 100644 eventcatalog/src/pages/chat/index.astro delete mode 100644 eventcatalog/src/pages/docs/_default-docs.mdx delete mode 100644 eventcatalog/src/pages/docs/index.astro create mode 100644 eventcatalog/src/pages/docs/teams/[id].mdx.ts create mode 100644 eventcatalog/src/pages/docs/users/[id].mdx.ts create mode 100644 eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts create mode 100644 eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro create mode 100644 eventcatalog/src/pages/schemas/explorer/_index.data.ts create mode 100644 eventcatalog/src/pages/schemas/explorer/index.astro delete mode 100644 eventcatalog/src/pages/schemas/index.astro create mode 100644 eventcatalog/src/stores/favorites-store.ts create mode 100644 eventcatalog/src/stores/sidebar-store.ts rename eventcatalog/src/utils/{ => collections}/channels.ts (56%) create mode 100644 eventcatalog/src/utils/collections/commands.ts create mode 100644 eventcatalog/src/utils/collections/entities.ts create mode 100644 eventcatalog/src/utils/collections/events.ts rename eventcatalog/src/utils/{ => collections}/messages.ts (61%) create mode 100644 eventcatalog/src/utils/collections/queries.ts create mode 100644 eventcatalog/src/utils/collections/teams.ts create mode 100644 eventcatalog/src/utils/collections/types.ts create mode 100644 eventcatalog/src/utils/collections/users.ts delete mode 100644 eventcatalog/src/utils/commands.ts delete mode 100644 eventcatalog/src/utils/entities.ts delete mode 100644 eventcatalog/src/utils/events.ts rename eventcatalog/src/utils/{collections => }/file-diffs.ts (98%) delete mode 100644 eventcatalog/src/utils/generators/index.ts delete mode 100644 eventcatalog/src/utils/queries.ts create mode 100644 eventcatalog/src/utils/resource-files.ts delete mode 100644 eventcatalog/src/utils/teams.ts delete mode 100644 eventcatalog/src/utils/users.ts rename examples/default/pages/{homepage.astro => _homepage.astro} (100%) delete mode 100644 images/sponsors/gravitee-logo-black.svg delete mode 100644 images/sponsors/gravitee-logo-white.webp delete mode 100644 images/sponsors/hookdeck.svg delete mode 100644 images/sponsors/oso-logo-green.png delete mode 100644 scripts/catalog-generator/README.md delete mode 100644 scripts/catalog-generator/generate-large-catalog.js delete mode 100644 scripts/catalog-generator/specs/DOMAIN/APP/1.0.0/APP.yaml rename scripts/{pagefind.js => start-server-locally.js} (62%) rename {__mocks__ => src/__mocks__}/astro-content.ts (100%) create mode 100644 src/utils/cli-logger.ts diff --git a/.astro/content-assets.mjs b/.astro/content-assets.mjs deleted file mode 100644 index 2b8b8234b..000000000 --- a/.astro/content-assets.mjs +++ /dev/null @@ -1 +0,0 @@ -export default new Map(); \ No newline at end of file diff --git a/.astro/content-modules.mjs b/.astro/content-modules.mjs deleted file mode 100644 index 2b8b8234b..000000000 --- a/.astro/content-modules.mjs +++ /dev/null @@ -1 +0,0 @@ -export default new Map(); \ No newline at end of file diff --git a/.astro/content.d.ts b/.astro/content.d.ts deleted file mode 100644 index c0082cc81..000000000 --- a/.astro/content.d.ts +++ /dev/null @@ -1,199 +0,0 @@ -declare module 'astro:content' { - export interface RenderResult { - Content: import('astro/runtime/server/index.js').AstroComponentFactory; - headings: import('astro').MarkdownHeading[]; - remarkPluginFrontmatter: Record; - } - interface Render { - '.md': Promise; - } - - export interface RenderedContent { - html: string; - metadata?: { - imagePaths: Array; - [key: string]: unknown; - }; - } -} - -declare module 'astro:content' { - type Flatten = T extends { [K: string]: infer U } ? U : never; - - export type CollectionKey = keyof AnyEntryMap; - export type CollectionEntry = Flatten; - - export type ContentCollectionKey = keyof ContentEntryMap; - export type DataCollectionKey = keyof DataEntryMap; - - type AllValuesOf = T extends any ? T[keyof T] : never; - type ValidContentEntrySlug = AllValuesOf< - ContentEntryMap[C] - >['slug']; - - export type ReferenceDataEntry< - C extends CollectionKey, - E extends keyof DataEntryMap[C] = string, - > = { - collection: C; - id: E; - }; - export type ReferenceContentEntry< - C extends keyof ContentEntryMap, - E extends ValidContentEntrySlug | (string & {}) = string, - > = { - collection: C; - slug: E; - }; - export type ReferenceLiveEntry = { - collection: C; - id: string; - }; - - /** @deprecated Use `getEntry` instead. */ - export function getEntryBySlug< - C extends keyof ContentEntryMap, - E extends ValidContentEntrySlug | (string & {}), - >( - collection: C, - // Note that this has to accept a regular string too, for SSR - entrySlug: E, - ): E extends ValidContentEntrySlug - ? Promise> - : Promise | undefined>; - - /** @deprecated Use `getEntry` instead. */ - export function getDataEntryById( - collection: C, - entryId: E, - ): Promise>; - - export function getCollection>( - collection: C, - filter?: (entry: CollectionEntry) => entry is E, - ): Promise; - export function getCollection( - collection: C, - filter?: (entry: CollectionEntry) => unknown, - ): Promise[]>; - - export function getLiveCollection( - collection: C, - filter?: LiveLoaderCollectionFilterType, - ): Promise< - import('astro').LiveDataCollectionResult, LiveLoaderErrorType> - >; - - export function getEntry< - C extends keyof ContentEntryMap, - E extends ValidContentEntrySlug | (string & {}), - >( - entry: ReferenceContentEntry, - ): E extends ValidContentEntrySlug - ? Promise> - : Promise | undefined>; - export function getEntry< - C extends keyof DataEntryMap, - E extends keyof DataEntryMap[C] | (string & {}), - >( - entry: ReferenceDataEntry, - ): E extends keyof DataEntryMap[C] - ? Promise - : Promise | undefined>; - export function getEntry< - C extends keyof ContentEntryMap, - E extends ValidContentEntrySlug | (string & {}), - >( - collection: C, - slug: E, - ): E extends ValidContentEntrySlug - ? Promise> - : Promise | undefined>; - export function getEntry< - C extends keyof DataEntryMap, - E extends keyof DataEntryMap[C] | (string & {}), - >( - collection: C, - id: E, - ): E extends keyof DataEntryMap[C] - ? string extends keyof DataEntryMap[C] - ? Promise | undefined - : Promise - : Promise | undefined>; - export function getLiveEntry( - collection: C, - filter: string | LiveLoaderEntryFilterType, - ): Promise, LiveLoaderErrorType>>; - - /** Resolve an array of entry references from the same collection */ - export function getEntries( - entries: ReferenceContentEntry>[], - ): Promise[]>; - export function getEntries( - entries: ReferenceDataEntry[], - ): Promise[]>; - - export function render( - entry: AnyEntryMap[C][string], - ): Promise; - - export function reference( - collection: C, - ): import('astro/zod').ZodEffects< - import('astro/zod').ZodString, - C extends keyof ContentEntryMap - ? ReferenceContentEntry> - : ReferenceDataEntry - >; - // Allow generic `string` to avoid excessive type errors in the config - // if `dev` is not running to update as you edit. - // Invalid collection names will be caught at build time. - export function reference( - collection: C, - ): import('astro/zod').ZodEffects; - - type ReturnTypeOrOriginal = T extends (...args: any[]) => infer R ? R : T; - type InferEntrySchema = import('astro/zod').infer< - ReturnTypeOrOriginal['schema']> - >; - - type ContentEntryMap = { - - }; - - type DataEntryMap = { - - }; - - type AnyEntryMap = ContentEntryMap & DataEntryMap; - - type ExtractLoaderTypes = T extends import('astro/loaders').LiveLoader< - infer TData, - infer TEntryFilter, - infer TCollectionFilter, - infer TError - > - ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError } - : { data: never; entryFilter: never; collectionFilter: never; error: never }; - type ExtractDataType = ExtractLoaderTypes['data']; - type ExtractEntryFilterType = ExtractLoaderTypes['entryFilter']; - type ExtractCollectionFilterType = ExtractLoaderTypes['collectionFilter']; - type ExtractErrorType = ExtractLoaderTypes['error']; - - type LiveLoaderDataType = - LiveContentConfig['collections'][C]['schema'] extends undefined - ? ExtractDataType - : import('astro/zod').infer< - Exclude - >; - type LiveLoaderEntryFilterType = - ExtractEntryFilterType; - type LiveLoaderCollectionFilterType = - ExtractCollectionFilterType; - type LiveLoaderErrorType = ExtractErrorType< - LiveContentConfig['collections'][C]['loader'] - >; - - export type ContentConfig = typeof import("../src/content.config.mjs"); - export type LiveContentConfig = never; -} diff --git a/.astro/data-store.json b/.astro/data-store.json index fc218cc0b..1edb66d67 100644 --- a/.astro/data-store.json +++ b/.astro/data-store.json @@ -1 +1,9 @@ -[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.16.0","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[]},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false,\"svgo\":false},\"legacy\":{\"collections\":false}}"] \ No newline at end of file +<<<<<<< HEAD +<<<<<<< HEAD +[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.16.0","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[]},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false,\"svgo\":false},\"legacy\":{\"collections\":false}}"] +======= +[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.16.5","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[]},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false,\"svgo\":false},\"legacy\":{\"collections\":false}}"] +>>>>>>> de19451 (feat(core): simplified eventcatalog chat (#1919)) +======= +[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.16.0","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[]},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false,\"svgo\":false},\"legacy\":{\"collections\":false}}"] +>>>>>>> ef2759e (New FHIR Viewer) diff --git a/.astro/settings.json b/.astro/settings.json index 64218f1ed..f0ed543ba 100644 --- a/.astro/settings.json +++ b/.astro/settings.json @@ -1,5 +1,13 @@ { "_variables": { +<<<<<<< HEAD +<<<<<<< HEAD "lastUpdateCheck": 1765300660767 +======= + "lastUpdateCheck": 1765539873455 +>>>>>>> de19451 (feat(core): simplified eventcatalog chat (#1919)) +======= + "lastUpdateCheck": 1765300660767 +>>>>>>> ef2759e (New FHIR Viewer) } } \ No newline at end of file diff --git a/.astro/types.d.ts b/.astro/types.d.ts deleted file mode 100644 index f964fe0cf..000000000 --- a/.astro/types.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/.changeset/beige-nails-wonder.md b/.changeset/beige-nails-wonder.md new file mode 100644 index 000000000..8b919a8b0 --- /dev/null +++ b/.changeset/beige-nails-wonder.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): added icons to spec files in sidebar diff --git a/.changeset/clever-turkeys-wink.md b/.changeset/clever-turkeys-wink.md new file mode 100644 index 000000000..11c8ba027 --- /dev/null +++ b/.changeset/clever-turkeys-wink.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +feat(core): visualizer now has presentation mode diff --git a/.changeset/curvy-clocks-complain.md b/.changeset/curvy-clocks-complain.md new file mode 100644 index 000000000..32ffba582 --- /dev/null +++ b/.changeset/curvy-clocks-complain.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +fix(core): fixed issue embedding pages on build diff --git a/.changeset/cyan-readers-hear.md b/.changeset/cyan-readers-hear.md new file mode 100644 index 000000000..7b439c040 --- /dev/null +++ b/.changeset/cyan-readers-hear.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +feat(core): added support for titles on admonitions diff --git a/.changeset/dirty-sheep-doubt.md b/.changeset/dirty-sheep-doubt.md new file mode 100644 index 000000000..32ffba582 --- /dev/null +++ b/.changeset/dirty-sheep-doubt.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +fix(core): fixed issue embedding pages on build diff --git a/.changeset/early-bottles-reply.md b/.changeset/early-bottles-reply.md new file mode 100644 index 000000000..3399a737b --- /dev/null +++ b/.changeset/early-bottles-reply.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +fix(core): fixed deployment of example catalogs on releases diff --git a/.changeset/eleven-terms-agree.md b/.changeset/eleven-terms-agree.md new file mode 100644 index 000000000..32ffba582 --- /dev/null +++ b/.changeset/eleven-terms-agree.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +fix(core): fixed issue embedding pages on build diff --git a/.changeset/five-cougars-smell.md b/.changeset/five-cougars-smell.md new file mode 100644 index 000000000..b9dae87b1 --- /dev/null +++ b/.changeset/five-cougars-smell.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): updated logger for the ecstudio watcher diff --git a/.changeset/fresh-yaks-yawn.md b/.changeset/fresh-yaks-yawn.md new file mode 100644 index 000000000..38c5654fe --- /dev/null +++ b/.changeset/fresh-yaks-yawn.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +feat(core): simplified eventcatalog chat diff --git a/.changeset/gold-dryers-sleep.md b/.changeset/gold-dryers-sleep.md new file mode 100644 index 000000000..930dab387 --- /dev/null +++ b/.changeset/gold-dryers-sleep.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +feat(core): added channel support in nav and search diff --git a/.changeset/good-humans-love.md b/.changeset/good-humans-love.md new file mode 100644 index 000000000..03df4fe74 --- /dev/null +++ b/.changeset/good-humans-love.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): removed unused icons on domain grid diff --git a/.changeset/happy-gorillas-fold.md b/.changeset/happy-gorillas-fold.md new file mode 100644 index 000000000..a57a509ee --- /dev/null +++ b/.changeset/happy-gorillas-fold.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): auth is now more explict opt in diff --git a/.changeset/khaki-humans-wonder.md b/.changeset/khaki-humans-wonder.md new file mode 100644 index 000000000..da3cf652c --- /dev/null +++ b/.changeset/khaki-humans-wonder.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): updated styles for v3 diff --git a/.changeset/khaki-scissors-boil.md b/.changeset/khaki-scissors-boil.md new file mode 100644 index 000000000..05d11b2dd --- /dev/null +++ b/.changeset/khaki-scissors-boil.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): removed some redundant files diff --git a/.changeset/late-zoos-scream.md b/.changeset/late-zoos-scream.md new file mode 100644 index 000000000..0901a5f73 --- /dev/null +++ b/.changeset/late-zoos-scream.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": major +--- + +feat(core): eventcatalog-v3 release diff --git a/.changeset/lemon-pugs-sing.md b/.changeset/lemon-pugs-sing.md new file mode 100644 index 000000000..d0d1a6398 --- /dev/null +++ b/.changeset/lemon-pugs-sing.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): refactored features into astro custom integrations diff --git a/.changeset/light-humans-mate.md b/.changeset/light-humans-mate.md new file mode 100644 index 000000000..10ef7346f --- /dev/null +++ b/.changeset/light-humans-mate.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): added empty state to nested sidebar diff --git a/.changeset/light-meals-play.md b/.changeset/light-meals-play.md new file mode 100644 index 000000000..05d11b2dd --- /dev/null +++ b/.changeset/light-meals-play.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): removed some redundant files diff --git a/.changeset/mighty-walls-watch.md b/.changeset/mighty-walls-watch.md new file mode 100644 index 000000000..3cf6fb67f --- /dev/null +++ b/.changeset/mighty-walls-watch.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +fix(core): fixed issues with nested sidebar state diff --git a/.changeset/plenty-crabs-cheat.md b/.changeset/plenty-crabs-cheat.md new file mode 100644 index 000000000..756b993f2 --- /dev/null +++ b/.changeset/plenty-crabs-cheat.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): added v3 beta message update diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000..4e3395436 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,40 @@ +{ + "mode": "pre", + "tag": "beta", + "initialVersions": { + "@eventcatalog/core": "2.65.1" + }, + "changesets": [ + "beige-nails-wonder", + "clever-turkeys-wink", + "curvy-clocks-complain", + "cyan-readers-hear", + "dirty-sheep-doubt", + "early-bottles-reply", + "eleven-terms-agree", + "five-cougars-smell", + "fresh-yaks-yawn", + "gold-dryers-sleep", + "good-humans-love", + "happy-gorillas-fold", + "khaki-humans-wonder", + "khaki-scissors-boil", + "late-zoos-scream", + "lemon-pugs-sing", + "light-humans-mate", + "light-meals-play", + "mighty-walls-watch", + "plenty-crabs-cheat", + "rotten-lamps-vanish", + "selfish-geese-wave", + "shaggy-adults-promise", + "sharp-files-occur", + "sweet-feet-reflect", + "tall-adults-end", + "tame-tools-protect", + "thin-lizards-guess", + "thirty-shrimps-argue", + "tiny-suns-lie", + "warm-scissors-help" + ] +} diff --git a/.changeset/rotten-lamps-vanish.md b/.changeset/rotten-lamps-vanish.md new file mode 100644 index 000000000..2b6f5c440 --- /dev/null +++ b/.changeset/rotten-lamps-vanish.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): updated packages diff --git a/.changeset/selfish-geese-wave.md b/.changeset/selfish-geese-wave.md new file mode 100644 index 000000000..091e0ac59 --- /dev/null +++ b/.changeset/selfish-geese-wave.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): fixing circular dep in JS diff --git a/.changeset/shaggy-adults-promise.md b/.changeset/shaggy-adults-promise.md new file mode 100644 index 000000000..a57a509ee --- /dev/null +++ b/.changeset/shaggy-adults-promise.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): auth is now more explict opt in diff --git a/.changeset/sharp-files-occur.md b/.changeset/sharp-files-occur.md new file mode 100644 index 000000000..8f5960e18 --- /dev/null +++ b/.changeset/sharp-files-occur.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +fix(core): mdx pages are added to teams and users diff --git a/.changeset/sweet-feet-reflect.md b/.changeset/sweet-feet-reflect.md new file mode 100644 index 000000000..f260f7ece --- /dev/null +++ b/.changeset/sweet-feet-reflect.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): updated cli logger diff --git a/.changeset/tall-adults-end.md b/.changeset/tall-adults-end.md new file mode 100644 index 000000000..49b3d876d --- /dev/null +++ b/.changeset/tall-adults-end.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +feat(core): updated homepage styles diff --git a/.changeset/tame-tools-protect.md b/.changeset/tame-tools-protect.md new file mode 100644 index 000000000..d72278c22 --- /dev/null +++ b/.changeset/tame-tools-protect.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +fix(core): removed duplicated edge labels diff --git a/.changeset/thin-lizards-guess.md b/.changeset/thin-lizards-guess.md new file mode 100644 index 000000000..e93234f5c --- /dev/null +++ b/.changeset/thin-lizards-guess.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +fix(core): fixed accessibility issues diff --git a/.changeset/thirty-shrimps-argue.md b/.changeset/thirty-shrimps-argue.md new file mode 100644 index 000000000..ebcd9a143 --- /dev/null +++ b/.changeset/thirty-shrimps-argue.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +feat(core): embedding visualizer can be embedded with animations diff --git a/.changeset/tiny-suns-lie.md b/.changeset/tiny-suns-lie.md new file mode 100644 index 000000000..002dcab13 --- /dev/null +++ b/.changeset/tiny-suns-lie.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +chore(core): updated react-syntax-highlighter diff --git a/.changeset/warm-scissors-help.md b/.changeset/warm-scissors-help.md new file mode 100644 index 000000000..7bc558918 --- /dev/null +++ b/.changeset/warm-scissors-help.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +fix(core): problems with asyncapi loading in the DOM diff --git a/.github/workflows/redeploy-eventcatalog-examples.yml b/.github/workflows/redeploy-eventcatalog-examples.yml new file mode 100644 index 000000000..ba613af80 --- /dev/null +++ b/.github/workflows/redeploy-eventcatalog-examples.yml @@ -0,0 +1,47 @@ +name: Redeploy EventCatalog Examples on core release +on: + workflow_run: + workflows: ["Release"] + types: + - completed +jobs: + redeploy-finance: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_commit.message, 'Version Packages') + steps: + - name: Redeploy EventCatalog Finance Example Catalog + run: curl -f -X POST "$VERCEL_DEPLOY_HOOK_URL" + env: + VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_FINANCE_EXAMPLE_CATALOG_URL }} + redeploy-healthcare: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_commit.message, 'Version Packages') + steps: + - name: Redeploy EventCatalog Healthcare Example Catalog + run: curl -f -X POST "$VERCEL_DEPLOY_HOOK_URL" + env: + VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_HEALTHCARE_EXAMPLE_CATALOG_URL }} + redeploy-demo: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_commit.message, 'Version Packages') + steps: + - name: Redeploy EventCatalog Demo (FlowMart) Example Catalog + run: curl -f -X POST "$VERCEL_DEPLOY_HOOK_URL" + env: + VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_DEMO_FLOWMART_CATALOG_URL }} + redeploy-saas: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_commit.message, 'Version Packages') + steps: + - name: Redeploy EventCatalog Demo (SaaS) Example Catalog + run: curl -f -X POST "$VERCEL_DEPLOY_HOOK_URL" + env: + VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_SAAS_EXAMPLE_CATALOG_URL }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 326a6a93e..da23c402e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ pnpm-debug.log* /examples/large-catalog/* eventcatalog/pnpm-lock.yaml eventcatalog/public/pagefind +/eventcatalog/src/pages/api/[...auth].ts .vscode/* @@ -37,10 +38,15 @@ git-push.sh src/__tests__/example-catalog-dependencies/dependencies +**/__tests__/catalog/ + eventcatalog/public/ai examples/default/public/ai +examples/default/eventcatalog.chat.js **/[...auth].ts +.astro/ + dev-scripts/ examples/e-commerce \ No newline at end of file diff --git a/.npmrc b/.npmrc index 319e41e69..c7ea0e1ed 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,7 @@ strict-peer-dependencies=false + +@nhsdigital:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} +always-auth=true + +registry=https://registry.npmjs.org/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a2da8a41..52078e4fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,156 @@ # @eventcatalog/core +## 3.0.0-beta.23 + +### Patch Changes + +- aff2f92: fix(core): removed duplicated edge labels + +## 3.0.0-beta.22 + +### Patch Changes + +- fcd3e9c: chore(core): removed some redundant files + +## 3.0.0-beta.21 + +### Patch Changes + +- 1a0bc7d: chore(core): removed some redundant files + +## 3.0.0-beta.20 + +### Patch Changes + +- 9ec4525: chore(core): refactored features into astro custom integrations + +## 3.0.0-beta.19 + +### Patch Changes + +- de19451: feat(core): simplified eventcatalog chat +- 7b1311b: chore(core): updated packages + +## 3.0.0-beta.18 + +### Patch Changes + +- 1cc63fa: chore(core): added v3 beta message update + +## 3.0.0-beta.17 + +### Patch Changes + +- c0372e5: feat(core): embedding visualizer can be embedded with animations + +## 3.0.0-beta.16 + +### Patch Changes + +- 786db2d: chore(core): added icons to spec files in sidebar + +## 3.0.0-beta.15 + +### Patch Changes + +- 5b4095e: fix(core): fixed accessibility issues + +## 3.0.0-beta.14 + +### Patch Changes + +- 57d1496: chore(core): auth is now more explict opt in +- c270a98: fix(core): problems with asyncapi loading in the DOM + +## 3.0.0-beta.13 + +### Patch Changes + +- 0bc73d3: chore(core): auth is now more explict opt in +- b8730a9: fix(core): mdx pages are added to teams and users + +## 3.0.0-beta.12 + +### Patch Changes + +- 8ed1960: chore(core): removed unused icons on domain grid + +## 3.0.0-beta.11 + +### Patch Changes + +- 39fbd2f: feat(core): added support for titles on admonitions +- dac4dc5: feat(core): updated homepage styles + +## 3.0.0-beta.10 + +### Patch Changes + +- c5592f1: fix(core): fixed issue embedding pages on build + +## 3.0.0-beta.9 + +### Patch Changes + +- 8db71c9: fix(core): fixed issue embedding pages on build + +## 3.0.0-beta.8 + +### Patch Changes + +- 507d14d: fix(core): fixed deployment of example catalogs on releases + +## 3.0.0-beta.7 + +### Patch Changes + +- 341279e: fix(core): fixed issue embedding pages on build + +## 3.0.0-beta.6 + +### Patch Changes + +- 713c535: chore(core): updated logger for the ecstudio watcher +- 2a32d7c: chore(core): added empty state to nested sidebar + +## 3.0.0-beta.5 + +### Patch Changes + +- 525c809: chore(core): updated react-syntax-highlighter + +## 3.0.0-beta.4 + +### Patch Changes + +- c43115d: feat(core): added channel support in nav and search + +## 3.0.0-beta.3 + +### Patch Changes + +- 08f0c81: chore(core): updated cli logger + +## 3.0.0-beta.2 + +### Patch Changes + +- 6c33b1f: feat(core): visualizer now has presentation mode + +## 3.0.0-beta.1 + +### Patch Changes + +- 35f760b: chore(core): updated styles for v3 +- ddc8af5: fix(core): fixed issues with nested sidebar state +- 8ca5436: chore(core): fixing circular dep in JS + +## 3.0.0-beta.0 + +### Major Changes + +- 1d1111d: feat(core): eventcatalog-v3 release + ## 2.65.1 ### Patch Changes diff --git a/Dockerfile.server b/Dockerfile.server index f4bab595d..4a7da649c 100644 --- a/Dockerfile.server +++ b/Dockerfile.server @@ -5,7 +5,7 @@ # FOR DEVELOPMENT ONLY, DO NOT USE THIS FOR PRODUCTION -FROM node:20-slim AS runtime +FROM node:20.19.6-trixie-slim AS runtime WORKDIR /app # Install pnpm diff --git a/README.md b/README.md index ee9dd5531..3304a359b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,7 @@
-

📖 EventCatalog

-

The open source tool to help you discover and document your event-driven architectures

- -[![MIT License][license-badge]][license] -[![PRs Welcome][prs-badge]][prs] - -[![](https://dcbadge.limes.pink/api/server/https://discord.gg/3rjaZMmrAm?style=flat)](https://discord.gg/3rjaZMmrAm) [](https://www.linkedin.com/in/david-boyne/) [![blog](https://img.shields.io/badge/blog-EDA--Visuals-brightgreen)](https://eda-visuals.boyney.io/?utm_source=event-catalog-gihub) + + @@ -25,134 +20,82 @@ - -EventCatalog - +EventCatalog -

Features: Documentation for Event Driven Architectures, Integration with any broker, Generator from your OpenAPI and AsyncAPI documents, Docs and Code, Markdown driven, Document Domains/Services/Messages/Schemas and more, Content versioning, Assign Owners, Schemas, OpenAPI, MDX Components and more...

+

+
+ EventCatalog is a documentation tool for software architectures — +
+ bring discoverability to complex systems. +

+

- -[![All Contributors](https://img.shields.io/badge/all_contributors-67-orange.svg?style=flat-square)](#contributors-) - +
-[Read the Docs](https://www.eventcatalog.dev/docs/development/getting-started/introduction) | [View Demo](https://demo.eventcatalog.dev) +[![main](https://github.com/event-catalog/eventcatalog/actions/workflows/verify-build.yml/badge.svg)](https://github.com/event-catalog/eventcatalog/actions/workflows/verify-build.yml) +[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/event-catalog/eventcatalog/blob/main/LICENSE) +[![npm version](https://badge.fury.io/js/@eventcatalog%2Fcore.svg)](https://badge.fury.io/js/@eventcatalog/core)
-
- -# Core Features - -- 📃 Document domains, services and messages ([demo](https://demo.eventcatalog.dev/docs)) -- 📊 Visualise your architecture ([demo](https://demo.eventcatalog.dev/visualiser/domains/Orders)) -- ⭐ Supports any Schema format (e.g Avro, JSON) ([demo](https://demo.eventcatalog.dev/docs/events/OrderConfirmed/0.0.1)) -- 🗂️ Document any code examples (Any code snippet) -- 💅 Custom MDX components ([read more](https://eventcatalog.dev/docs/development/components/using-components)) -- 🗄️ Version domains, services and messages -- ⭐ Discoverability feature (search, filter and more) ([demo](https://demo.eventcatalog.dev/discover/events)) -- ⭐ Document teams and users ([demo](https://demo.eventcatalog.dev/docs/teams/full-stack)) -- 🤖 Automate your catalogs with [generators](https://www.eventcatalog.dev/docs/development/plugins/plugin-overview) (e.g generate your catalogs from your [AsyncAPI](https://www.eventcatalog.dev/docs/asyncapi)/[OpenAPI](https://www.eventcatalog.dev/docs/openapi) documents) -- 👨🏼‍💻 Follows [Docs as code](https://www.writethedocs.org/guide/docs-as-code/) principles -- ⭐ And much more... - - -# The problem - -Event-driven architectures are becoming more popular, giving us the ability to write decoupled architectures and use messages as away to communicate between domains/teams. - -When starting with event-driven architectures you may have a handful of services and messages. As this scales with your team and organization it becomes very hard to manage and govern this. -Over a period of time more events are added to our domain, requirements change, and our architecture scales. + -Think of EventCatalog as a website generator that allows you to document your event architectures powered by markdown. -EventCatalog is focused on discovery and documentation and allows you to: -- Document Domains/Services/Messages/Schemas/Code Examples and more... -- Visually shows relationships between upstream/downstream services using your Events -- Allows you to version your documentation and supports changelogs -- Add owners to domains,services and messages so your teams know who owns which parts of your domain -- And much more... -EventCatalog is technology agnostic, which means you can integrate your Catalog with any EDA technology of your choice and any schema formats. -EventCatalog supports a [Plugin Architecture](https://github.com/event-catalog/generators) which lets you generate documentation from your systems including OpenAPI, AsyncAPI, Event Brokers and more. + -# Getting Started + + + -You should be able to get setup within minutes if you head over to our documentation to get started 👇 + -➡️ [Get Started](https://www.eventcatalog.dev/docs/development/getting-started/installation) +
-Or run this command to build a new catalog +## Install +The **recommended** way to install the latest version of EventCatalog is by running the command below: ``` npx @eventcatalog/create-eventcatalog@latest my-catalog ``` -# Demo - -Here is an example of a Retail system using domains, services and messages. +Looking for help? Start with our [Getting Started](https://www.eventcatalog.dev/docs/development/starting-a-new-project/installation) guide -[demo.eventcatalog.dev](https://demo.eventcatalog.dev) +## Documentation +Visit our [official documentation](https://www.eventcatalog.dev/docs/development/getting-started). -You can see the markdown files that generated the website in the GitHub repo under [examples](/examples). +## Support +Having trouble? Get help in the official [EventCatalog Discord](https://discord.gg/3rjaZMmrAm). -# Enterprise support +## Demos -Interested in collaborating with us? Our offerings include dedicated support, priority assistance, feature development, custom integrations, and more. +Here are some examples of EventCatalog in action: -Find more details on our [services page](https://eventcatalog.dev/services). +- [Finance System](https://eventcatalog-examples-finance.vercel.app/) +- [Healthcare System](https://eventcatalog-examples-healthcare.vercel.app/) +- [E-Commerce System](https://demo.eventcatalog.dev/) +- [SaaS System](https://eventcatalog-examples-saas.vercel.app/) -# Looking for v1? -- Documentation: https://v1.eventcatalog.dev -- Code: https://github.com/event-catalog/eventcatalog/tree/v1 - -_Still using v1 of EventCatalog? We recommnded upgrading to the latest version. [Read more in the migration guide](https://eventcatalog.dev/docs/development/guides/upgrading-from-version-1)._ - - -# Contributing +## Contributing If you have any questions, features or issues please raise any issue or pull requests you like. We will try my best to get back to you. You can find the [contributing guidelines here](https://eventcatalog.dev/docs/contributing/overview). -## Running the project locally - -1. Clone the repo -1. Install required dependencies `pnpm install` -1. Run the command `pnpm run start:catalog` - - This will start the catalog found in `/examples` repo, locally on your machine - -[license-badge]: https://img.shields.io/github/license/event-catalog/eventcatalog.svg?color=yellow -[license]: https://github.com/event-catalog/eventcatalog/blob/main/LICENSE -[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square -[prs]: http://makeapullrequest.com -[github-watch-badge]: https://img.shields.io/github/watchers/event-catalog/eventcatalog.svg?style=social -[github-watch]: https://github.com/event-catalog/eventcatalog/watchers -[github-star-badge]: https://img.shields.io/github/stars/event-catalog/eventcatalog.svg?style=social -[github-star]: https://github.com/event-catalog/eventcatalog/stargazers - ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): diff --git a/default-files-for-collections/changelogs.md b/default-files-for-collections/changelogs.md deleted file mode 100644 index c97be0077..000000000 --- a/default-files-for-collections/changelogs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -id: empty ---- - - diff --git a/default-files-for-collections/channels.md b/default-files-for-collections/channels.md deleted file mode 100644 index 089fe4913..000000000 --- a/default-files-for-collections/channels.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -id: empty -name: empty -version: 0.0.1 -hidden: true ---- - - diff --git a/default-files-for-collections/commands.md b/default-files-for-collections/commands.md deleted file mode 100644 index 089fe4913..000000000 --- a/default-files-for-collections/commands.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -id: empty -name: empty -version: 0.0.1 -hidden: true ---- - - diff --git a/default-files-for-collections/domains.md b/default-files-for-collections/domains.md deleted file mode 100644 index 089fe4913..000000000 --- a/default-files-for-collections/domains.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -id: empty -name: empty -version: 0.0.1 -hidden: true ---- - - diff --git a/default-files-for-collections/events.md b/default-files-for-collections/events.md deleted file mode 100644 index 089fe4913..000000000 --- a/default-files-for-collections/events.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -id: empty -name: empty -version: 0.0.1 -hidden: true ---- - - diff --git a/default-files-for-collections/flows.md b/default-files-for-collections/flows.md deleted file mode 100644 index effd29cc4..000000000 --- a/default-files-for-collections/flows.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -id: empty -steps: - - id: "empty" - title: "empty" -name: empty -version: 0.0.1 -hidden: true ---- - - diff --git a/default-files-for-collections/queries.md b/default-files-for-collections/queries.md deleted file mode 100644 index 089fe4913..000000000 --- a/default-files-for-collections/queries.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -id: empty -name: empty -version: 0.0.1 -hidden: true ---- - - diff --git a/default-files-for-collections/services.md b/default-files-for-collections/services.md deleted file mode 100644 index 089fe4913..000000000 --- a/default-files-for-collections/services.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -id: empty -name: empty -version: 0.0.1 -hidden: true ---- - - diff --git a/default-files-for-collections/ubiquitousLanguages.md b/default-files-for-collections/ubiquitousLanguages.md deleted file mode 100644 index aaba1d03b..000000000 --- a/default-files-for-collections/ubiquitousLanguages.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -id: ubiquitous-language -name: Ubiquitous Language -summary: A shared language used by all team members to communicate about the system. -description: A shared language used by all team members to communicate about the system. ---- - \ No newline at end of file diff --git a/eventcatalog/astro.config.mjs b/eventcatalog/astro.config.mjs index 9035b1045..d011c92f6 100644 --- a/eventcatalog/astro.config.mjs +++ b/eventcatalog/astro.config.mjs @@ -20,12 +20,14 @@ import rehypeExpressiveCode from 'rehype-expressive-code' import config from './eventcatalog.config'; import expressiveCode from 'astro-expressive-code'; import ecstudioWatcher from './integrations/ecstudio-watcher.mjs'; +import eventCatalogIntegration from './integrations/eventcatalog-features.ts'; const projectDirectory = process.env.PROJECT_DIR || process.cwd(); const base = config.base || '/'; const host = config.host || false; const compress = config.compress ?? false; + const expressiveCodeConfig = { themes: ['andromeeda'], defaultProps: { @@ -89,6 +91,7 @@ export default defineConfig({ CSS: false, }), ecstudioWatcher(), + eventCatalogIntegration(), ].filter(Boolean), vite: { define: { @@ -119,7 +122,7 @@ export default defineConfig({ } }, ssr: { - external: ['eventcatalog.auth.js'], + external: ['eventcatalog.auth.js', 'eventcatalog.chat.js'], } } }); diff --git a/eventcatalog/integrations/ecstudio-watcher.mjs b/eventcatalog/integrations/ecstudio-watcher.mjs index 94aa28f0f..6c17cca42 100644 --- a/eventcatalog/integrations/ecstudio-watcher.mjs +++ b/eventcatalog/integrations/ecstudio-watcher.mjs @@ -55,7 +55,7 @@ export default function ecstudioWatcher() { // Also add the root directory to watch for new files server.watcher.add(rootDir); - console.log('Set up dynamic .ecstudio file watcher with content refresh'); + // console.log('Set up dynamic .ecstudio file watcher with content refresh'); }, }, }; diff --git a/eventcatalog/integrations/eventcatalog-features.ts b/eventcatalog/integrations/eventcatalog-features.ts new file mode 100644 index 000000000..6b0166ff2 --- /dev/null +++ b/eventcatalog/integrations/eventcatalog-features.ts @@ -0,0 +1,69 @@ +import type { AstroIntegration } from 'astro'; +import path from 'path'; +import { + isEventCatalogChatEnabled, + isAuthEnabled, + isEventCatalogScaleEnabled, + isEventCatalogStarterEnabled, +} from '../src/utils/feature'; + +const catalogDirectory = process.env.CATALOG_DIR || process.cwd(); + +const configureAuthentication = (params: { + injectRoute: (route: { pattern: string; entrypoint: string }) => void; + addMiddleware: (middleware: { entrypoint: string; order: 'pre' }) => void; +}) => { + params.injectRoute({ + pattern: '/api/[...auth]', + entrypoint: path.join(catalogDirectory, 'src/enterprise/auth/[...auth].ts'), + }); + params.injectRoute({ + pattern: '/auth/login', + entrypoint: path.join(catalogDirectory, 'src/enterprise/auth/login.astro'), + }); + params.injectRoute({ + pattern: '/auth/error', + entrypoint: path.join(catalogDirectory, 'src/enterprise/auth/error.astro'), + }); + + params.injectRoute({ + pattern: '/unauthorized', + entrypoint: path.join(catalogDirectory, 'src/enterprise/auth/unauthorized.astro'), + }); + + // Add the authentication middleware + params.addMiddleware({ + entrypoint: path.join(catalogDirectory, 'src/enterprise/auth/middleware/middleware.ts'), + order: 'pre', + }); +}; + +export default function eventCatalogIntegration(): AstroIntegration { + return { + name: 'eventcatalog', + hooks: { + 'astro:config:setup': (params) => { + // Handle routes for AI features + if (isEventCatalogChatEnabled()) { + params.injectRoute({ + pattern: '/api/chat', + entrypoint: path.join(catalogDirectory, 'src/enterprise/ai/chat-api.ts'), + }); + } + + // Handle routes for authentication + if (isAuthEnabled()) { + configureAuthentication(params); + } + + // If non paying user, add the plans route into the project + if (!isEventCatalogStarterEnabled() && !isEventCatalogScaleEnabled()) { + params.injectRoute({ + pattern: '/plans', + entrypoint: path.join(catalogDirectory, 'src/enterprise/plans/index.astro'), + }); + } + }, + }, + }; +} diff --git a/eventcatalog/public/icons/asyncapi-black.svg b/eventcatalog/public/icons/asyncapi-black.svg new file mode 100644 index 000000000..8217adbc8 --- /dev/null +++ b/eventcatalog/public/icons/asyncapi-black.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/eventcatalog/public/icons/graphql-black.svg b/eventcatalog/public/icons/graphql-black.svg new file mode 100644 index 000000000..88c722d51 --- /dev/null +++ b/eventcatalog/public/icons/graphql-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eventcatalog/public/icons/openapi-black.svg b/eventcatalog/public/icons/openapi-black.svg new file mode 100644 index 000000000..c8b9aaa78 --- /dev/null +++ b/eventcatalog/public/icons/openapi-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/eventcatalog/public/logo_old.png b/eventcatalog/public/logo_old.png deleted file mode 100644 index 9df4b958f86ae134163674c0edb1778efb5c015c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54304 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 diff --git a/eventcatalog/src/__tests__/middleware-auth.spec.ts b/eventcatalog/src/__tests__/middleware-auth.spec.ts index d7849ce4e..c076d8ff0 100644 --- a/eventcatalog/src/__tests__/middleware-auth.spec.ts +++ b/eventcatalog/src/__tests__/middleware-auth.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { findMatchingRule, matchesPattern } from '../middleware-auth'; +import { findMatchingRule, matchesPattern } from '../enterprise/auth/middleware/middleware-auth'; describe('middleware-auth', () => { describe('matchesPattern', () => { diff --git a/eventcatalog/src/components/ChatPanel/ChatPanel.tsx b/eventcatalog/src/components/ChatPanel/ChatPanel.tsx new file mode 100644 index 000000000..72239e13b --- /dev/null +++ b/eventcatalog/src/components/ChatPanel/ChatPanel.tsx @@ -0,0 +1,821 @@ +import { useEffect, useRef, useCallback, useState } from 'react'; +import { X, Sparkles, Square, Trash2, BookOpen, Copy, Check, Maximize2, Minimize2 } from 'lucide-react'; +import { useChat } from '@ai-sdk/react'; +import ReactMarkdown from 'react-markdown'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; +import { lastAssistantMessageIsCompleteWithToolCalls } from 'ai'; +import * as Dialog from '@radix-ui/react-dialog'; + +// Code block component with copy functionality +const CodeBlock = ({ language, children }: { language: string; children: string }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(children); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ + + {children} + +
+ ); +}; + +// Get time-based greeting +const getGreeting = () => { + const hour = new Date().getHours(); + if (hour < 12) return 'Good morning'; + if (hour < 18) return 'Good afternoon'; + return 'Good evening'; +}; + +// ============================================ +// SUGGESTED QUESTIONS CONFIGURATION +// ============================================ +// Each config has a pattern (regex) to match the URL path +// and an array of questions to show. Questions are checked +// in order - first matching pattern wins. +// ============================================ + +interface SuggestedQuestion { + label: string; + prompt: string; +} + +interface QuestionConfig { + pattern: RegExp; + questions: SuggestedQuestion[]; +} + +const suggestedQuestionsConfig: QuestionConfig[] = [ + // Message pages (events, commands, queries) - most specific first + { + pattern: /^\/docs\/(events|commands|queries)\/.+/, + questions: [ + { label: 'Which services publish this?', prompt: 'Who produces this message?' }, + { label: 'Which services subscribe to this?', prompt: 'Who consumes this message?' }, + { label: 'View the message schema', prompt: 'Show me the schema for this message' }, + { label: 'What breaks if this changes?', prompt: 'What services would be affected if this message changes?' }, + ], + }, + // AsyncAPI specification page + { + pattern: /^\/docs\/services\/.+\/asyncapi\/.+/, + questions: [ + { label: 'Summarize this API', prompt: 'Help me understand this AsyncAPI specification' }, + { label: 'Show all channels', prompt: 'What channels are defined in this AsyncAPI spec?' }, + { label: 'How do I authenticate?', prompt: 'What authentication is required for this service?' }, + { label: 'What message formats are used?', prompt: 'What are the message formats and schemas?' }, + ], + }, + // OpenAPI specification page + { + pattern: /^\/docs\/services\/.+\/spec\/.+/, + questions: [ + { label: 'Summarize this API', prompt: 'Help me understand this OpenAPI specification' }, + { label: 'Show all endpoints', prompt: 'What endpoints are available in this API?' }, + { label: 'How do I authenticate?', prompt: 'What authentication is required for this API?' }, + { label: 'What are the request & response formats?', prompt: 'What are the request and response formats?' }, + ], + }, + // Services page + { + pattern: /^\/docs\/services\/.+/, + questions: [ + { label: 'Who owns this service?', prompt: 'Who owns this service and how do I contact them?' }, + { label: 'What does this depend on?', prompt: 'What are the upstream and downstream dependencies of this service?' }, + { label: 'How do I integrate with this?', prompt: 'How do I integrate with this service?' }, + { label: 'What messages does this publish?', prompt: 'What messages does this service produce?' }, + ], + }, + // Domains page + { + pattern: /^\/docs\/domains\/.+/, + questions: [ + { label: 'What services are in this domain?', prompt: 'What services belong to this domain?' }, + { label: 'What business capability is this?', prompt: 'What business capability does this domain represent?' }, + { label: 'What events come from this domain?', prompt: 'What events are published by this domain?' }, + { label: 'Who owns this domain?', prompt: 'Who owns this domain and how do I contact them?' }, + ], + }, + // Any other docs page + { + pattern: /^\/docs\/.+/, + questions: [ + { label: 'Tell me about this', prompt: 'Tell me more about this page' }, + { label: 'Who is responsible for this?', prompt: 'Who owns this resource?' }, + { label: 'What else is related to this?', prompt: 'What other resources are related to this?' }, + ], + }, + // Default questions (fallback) + { + pattern: /.*/, + questions: [ + { label: 'What domains do we have?', prompt: 'What domains are in my catalog?' }, + { label: 'Show me all services', prompt: 'What services do I have?' }, + { label: 'What changed recently?', prompt: 'What are the most recent changes in the catalog?' }, + { label: 'How does data flow between services?', prompt: 'Show me how data flows between services' }, + ], + }, +]; + +// Get suggested questions based on current URL path +const getSuggestedQuestions = (pathname: string): SuggestedQuestion[] => { + for (const config of suggestedQuestionsConfig) { + if (config.pattern.test(pathname)) { + return config.questions; + } + } + // Fallback to last config (default) + return suggestedQuestionsConfig[suggestedQuestionsConfig.length - 1].questions; +}; + +interface ChatPanelProps { + isOpen: boolean; + onClose: () => void; +} + +const PANEL_WIDTH = 400; + +// Staggered fade-in animation styles +const fadeInStyles = { + header: { + animation: 'fadeInDown 0.3s ease-out 0.1s both', + }, + content: { + animation: 'fadeInDown 0.3s ease-out 0.2s both', + }, + input: { + animation: 'fadeInDown 0.3s ease-out 0.3s both', + }, +}; + +// Helper to extract text content from message parts +const getMessageContent = (message: { parts?: Array<{ type: string; text?: string }> }): string => { + if (!message.parts) return ''; + return message.parts + .filter((part): part is { type: 'text'; text: string } => part.type === 'text' && typeof part.text === 'string') + .map((part) => part.text) + .join(''); +}; + +// Skeleton loading component +const SkeletonLoader = () => ( +
+
+
+
+
+
+); + +const ChatPanel = ({ isOpen, onClose }: ChatPanelProps) => { + const inputRef = useRef(null); + const modalInputRef = useRef(null); + const messagesEndRef = useRef(null); + const modalMessagesEndRef = useRef(null); + const [inputValue, setInputValue] = useState(''); + const [isWaitingForResponse, setIsWaitingForResponse] = useState(false); + const [pathname, setPathname] = useState(''); + const [isFullscreen, setIsFullscreen] = useState(false); + + // Get current pathname on mount and when panel opens + useEffect(() => { + setPathname(window.location.pathname); + }, [isOpen]); + + const suggestedQuestions = getSuggestedQuestions(pathname); + + const { messages, sendMessage, stop, status, setMessages } = useChat({ + sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, + }); + + // Check if the assistant has started outputting content + const lastAssistantMessage = [...messages].reverse().find((m) => m.role === 'assistant'); + const assistantHasContent = lastAssistantMessage?.parts?.some( + (p) => p.type === 'text' && (p as { type: 'text'; text: string }).text.length > 0 + ); + + // Clear waiting state once assistant starts outputting or on error + useEffect(() => { + if (assistantHasContent || status === 'error') { + setIsWaitingForResponse(false); + } + }, [assistantHasContent, status]); + + const isStreaming = status === 'streaming' && assistantHasContent; + const isThinking = isWaitingForResponse || ((status === 'submitted' || status === 'streaming') && !assistantHasContent); + const isLoading = isThinking || isStreaming; + + // Scroll to bottom when new messages arrive + const scrollToBottom = useCallback(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, []); + + useEffect(() => { + scrollToBottom(); + // Also scroll modal messages + modalMessagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages, scrollToBottom]); + + // Focus modal input when fullscreen opens + useEffect(() => { + if (isFullscreen && !isLoading) { + setTimeout(() => { + modalInputRef.current?.focus(); + }, 100); + } + }, [isFullscreen, isLoading]); + + // Handle escape key to close + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) { + onClose(); + } + // Focus input on CMD+I or CTRL+I + if ((e.metaKey || e.ctrlKey) && e.key === 'i' && isOpen) { + e.preventDefault(); + inputRef.current?.focus(); + } + }; + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [isOpen, onClose]); + + // Focus input when opened and not loading + useEffect(() => { + if (isOpen && !isLoading) { + setTimeout(() => { + inputRef.current?.focus(); + }, 100); + } + }, [isOpen, isLoading]); + + // Add/remove padding to main content when sidebar panel is open + useEffect(() => { + const contentEl = document.getElementById('content'); + if (!contentEl) return; + + // Add transition if not already present + if (!contentEl.style.transition) { + contentEl.style.transition = 'padding-right 300ms cubic-bezier(0.16, 1, 0.3, 1)'; + } + + // Only add padding when panel is open AND not in fullscreen mode + if (isOpen && !isFullscreen) { + contentEl.style.paddingRight = '23rem'; + } else { + contentEl.style.paddingRight = '0'; + } + + // Cleanup on unmount + return () => { + contentEl.style.paddingRight = '0'; + }; + }, [isOpen, isFullscreen]); + + // Submit message handler + const submitMessage = useCallback( + (text: string) => { + if (!text.trim() || isLoading) return; + setInputValue(''); + setIsWaitingForResponse(true); + sendMessage({ text }); + }, + [isLoading, sendMessage] + ); + + // Handle suggested action clicks + const handleSuggestedAction = useCallback( + (prompt: string) => { + submitMessage(prompt); + }, + [submitMessage] + ); + + // Handle textarea enter key + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + submitMessage(inputValue); + } + }, + [inputValue, submitMessage] + ); + + // Handle form submit + const handleSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + submitMessage(inputValue); + }, + [inputValue, submitMessage] + ); + + const hasMessages = messages.length > 0; + + return ( + <> + {/* Keyframes for fade-in animation */} + + + {/* Panel - hidden when fullscreen modal is open */} + {!isFullscreen && ( +
+ {/* Purple accent line at top */} +
+ + {/* Header */} +
+
+
+
+ + +
+ EventCatalog Assistant +
+
+ + {hasMessages && ( + + )} + +
+
+ {/* Thinking indicator */} + {isThinking && ( +
+
+ Thinking... +
+ )} +
+ + {/* Content */} +
+ {/* Messages or Welcome area */} +
+ {!hasMessages ? ( + /* Welcome area */ +
+ {/* Center content */} +
+ {/* Animated Icon */} +
+
+
+ + +
+
+ + {/* Greeting with gradient */} +

+ {getGreeting()} +

+

I'm here to help you explore your architecture.

+
+ + {/* Suggested questions */} +
+ {suggestedQuestions.map((question, index) => ( + + ))} +
+
+ ) : ( + /* Messages area */ + + + {/* Input area (Fixed at bottom) */} +
+
+
+ setInputValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + submitMessage(inputValue); + } + }} + placeholder="Ask anything about your architecture..." + disabled={isLoading} + className="w-full px-3 py-2.5 pr-16 bg-transparent text-gray-900 placeholder-gray-400 focus:outline-none text-sm disabled:opacity-50 rounded-xl" + /> +
+ {isStreaming ? ( + + ) : ( + + )} +
+
+
+ +

AI can make mistakes. Verify important info.

+
+
+
+ )} + + {/* Fullscreen Modal */} + { + setIsFullscreen(open); + // If modal is being closed (clicking outside, etc.), close the chat entirely + if (!open) { + onClose(); + } + }} + > + + + + {/* Purple accent line at top */} +
+ + {/* Modal Header */} +
+
+
+ + +
+ EventCatalog Assistant +
+
+ {hasMessages && ( + + )} + + +
+
+ + {/* Thinking indicator */} + {isThinking && ( +
+
+ Thinking... +
+ )} + + {/* Modal Content */} +
+ {!hasMessages ? ( + /* Welcome area */ +
+ {/* Animated Icon */} +
+
+
+ + +
+
+ + {/* Greeting with gradient */} +

+ {getGreeting()} +

+

I'm here to help you explore your architecture.

+ + {/* Suggested questions */} +
+ {suggestedQuestions.map((question, index) => ( + + ))} +
+
+ ) : ( + /* Messages area */ +
+ + {/* Modal Input area */} +
+
+
+ setInputValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + submitMessage(inputValue); + } + }} + placeholder="Ask anything about your architecture..." + disabled={isLoading} + className="w-full px-4 py-3 pr-20 bg-transparent text-gray-900 placeholder-gray-400 focus:outline-none text-sm disabled:opacity-50 rounded-xl" + /> +
+ {isStreaming ? ( + + ) : ( + + )} +
+
+
+

AI can make mistakes. Verify important info.

+
+ + + + + ); +}; + +export default ChatPanel; diff --git a/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx b/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx new file mode 100644 index 000000000..379dbd8d8 --- /dev/null +++ b/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx @@ -0,0 +1,24 @@ +import { useState } from 'react'; +import { Sparkles } from 'lucide-react'; +import ChatPanel from './ChatPanel'; + +const ChatPanelButton = () => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + + + setIsOpen(false)} /> + + ); +}; + +export default ChatPanelButton; diff --git a/eventcatalog/src/components/CopyAsMarkdown.tsx b/eventcatalog/src/components/CopyAsMarkdown.tsx index 0360c3bf4..edda83a98 100644 --- a/eventcatalog/src/components/CopyAsMarkdown.tsx +++ b/eventcatalog/src/components/CopyAsMarkdown.tsx @@ -1,5 +1,5 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; -import { Copy, FileText, MessageCircleQuestion, ChevronDownIcon, ExternalLink, PenSquareIcon } from 'lucide-react'; +import { Copy, FileText, MessageCircleQuestion, ChevronDownIcon, ExternalLink, PenSquareIcon, RssIcon } from 'lucide-react'; import React, { useState, isValidElement } from 'react'; import type { Schema } from '@utils/collections/schemas'; import { buildUrl, toMarkdownUrl } from '@utils/url-builder'; @@ -48,12 +48,14 @@ export function CopyPageMenu({ chatEnabled = false, editUrl, markdownDownloadEnabled = false, + rssFeedEnabled = false, }: { schemas: Schema[]; chatQuery?: string; chatEnabled: boolean; editUrl: string; markdownDownloadEnabled: boolean; + rssFeedEnabled: boolean; }) { // Define available actions const availableActions = { @@ -62,6 +64,7 @@ export function CopyPageMenu({ copySchemas: schemas.length > 0, viewMarkdown: markdownDownloadEnabled, chat: chatEnabled, + rssFeed: rssFeedEnabled, }; // Check if any actions are available @@ -113,6 +116,13 @@ export function CopyPageMenu({ icon: MessageCircleQuestion, }; } + if (availableActions.rssFeed) { + return { + type: 'rssFeed', + text: 'RSS Feed', + icon: RssIcon, + }; + } return null; }; @@ -287,6 +297,14 @@ export function CopyPageMenu({ )} + {availableActions.rssFeed && ( + window.open(buildUrl(`/rss/all/rss.xml`), '_blank')} + > + + + )} {availableActions.chat && ( -
- -
- -
- 0 -
- -
- {label} - - { - isRSSEnabled && ( -
- ) - } -
- - diff --git a/eventcatalog/src/components/FavoriteButton.tsx b/eventcatalog/src/components/FavoriteButton.tsx new file mode 100644 index 000000000..ca78a9e1e --- /dev/null +++ b/eventcatalog/src/components/FavoriteButton.tsx @@ -0,0 +1,54 @@ +import React, { useState, useEffect } from 'react'; +import { StarIcon as StarIconOutline } from '@heroicons/react/24/outline'; +import { StarIcon as StarIconSolid } from '@heroicons/react/24/solid'; +import { useStore } from '@nanostores/react'; +import { favoritesStore, toggleFavorite, type FavoriteItem } from '../stores/favorites-store'; + +interface FavoriteButtonProps { + nodeKey: string; + title: string; + badge?: string; + href?: string; + size?: 'sm' | 'md' | 'lg'; +} + +export default function FavoriteButton({ nodeKey, title, badge, href, size = 'md' }: FavoriteButtonProps) { + const favorites = useStore(favoritesStore); + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + const isFavorite = isClient && favorites.some((fav) => fav.nodeKey === nodeKey); + + const sizeClasses = { + sm: 'h-4 w-4', + md: 'h-6 w-6', + lg: 'h-8 w-8', + }; + + const handleToggleFavorite = () => { + const favoriteItem: FavoriteItem = { + nodeKey, + path: [], + title, + badge, + href, + }; + toggleFavorite(favoriteItem); + }; + + return ( + + ); +} diff --git a/eventcatalog/src/components/Grids/DomainGrid.tsx b/eventcatalog/src/components/Grids/DomainGrid.tsx index e71a18fc2..fb126a82e 100644 --- a/eventcatalog/src/components/Grids/DomainGrid.tsx +++ b/eventcatalog/src/components/Grids/DomainGrid.tsx @@ -1,390 +1,412 @@ -import { useState, useMemo, useEffect } from 'react'; +import { memo, useMemo, useState } from 'react'; import { ServerIcon, - EnvelopeIcon, RectangleGroupIcon, - Squares2X2Icon, - QueueListIcon, + BoltIcon, + ChatBubbleLeftIcon, + MagnifyingGlassIcon, CircleStackIcon, + ChevronDownIcon, + ChevronUpIcon, + ArrowsPointingOutIcon, } from '@heroicons/react/24/outline'; -import { buildUrlWithParams, buildUrl } from '@utils/url-builder'; -import type { CollectionEntry } from 'astro:content'; -import { type CollectionMessageTypes } from '@types'; -import { getCollectionStyles } from './utils'; -import { SearchBar } from './components'; +import { buildUrl } from '@utils/url-builder'; import { BoxIcon } from 'lucide-react'; -export interface ExtendedDomain extends CollectionEntry<'domains'> { - sends: CollectionEntry[]; - receives: CollectionEntry[]; - services: CollectionEntry<'services'>[]; - domains: CollectionEntry<'domains'>[]; -} +// ============================================ +// Types +// ============================================ interface DomainGridProps { - domains: ExtendedDomain[]; - embeded: boolean; + domain: any; } -export default function DomainGrid({ domains, embeded }: DomainGridProps) { - const [searchQuery, setSearchQuery] = useState(''); - const [isMultiColumn, setIsMultiColumn] = useState(false); - - useEffect(() => { - if (typeof window !== 'undefined') { - const saved = localStorage.getItem('EventCatalog:ArchitectureColumnLayout'); - if (saved !== null) { - setIsMultiColumn(saved === 'multi'); - } - } - }, []); - - const toggleColumnLayout = () => { - const newValue = !isMultiColumn; - setIsMultiColumn(newValue); - if (typeof window !== 'undefined') { - localStorage.setItem('EventCatalog:ArchitectureColumnLayout', newValue ? 'multi' : 'single'); - } - }; - - const filteredDomains = useMemo(() => { - let result = [...domains]; - - // Filter by search query - if (searchQuery) { - const query = searchQuery.toLowerCase(); - result = result.filter( - (domain) => - domain.data.name?.toLowerCase().includes(query) || - domain.data.summary?.toLowerCase().includes(query) || - domain.data.services?.some((service: any) => service.data.name.toLowerCase().includes(query)) || - domain.sends?.some((message: any) => message.data.name.toLowerCase().includes(query)) || - domain.receives?.some((message: any) => message.data.name.toLowerCase().includes(query)) - ); - } - - // Sort by name by default - result.sort((a, b) => (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id)); - - return result; - }, [domains, searchQuery]); +// ============================================ +// Helper functions +// ============================================ + +const getMessageIcon = (collection: string) => { + switch (collection) { + case 'events': + return { Icon: BoltIcon, color: 'orange' }; + case 'commands': + return { Icon: ChatBubbleLeftIcon, color: 'blue' }; + case 'queries': + return { Icon: MagnifyingGlassIcon, color: 'green' }; + default: + return { Icon: BoltIcon, color: 'gray' }; + } +}; + +// ============================================ +// Simple Sub-components +// ============================================ + +const EntityBadge = memo(({ entity }: { entity: any }) => { + const id = entity?.data?.id || entity?.id; + const name = entity?.data?.name || entity?.name || id; return ( -
- {/* Breadcrumb */} - - -
-
-
-

- Domains ({filteredDomains.length}) -

-

Browse and manage domains in your event-driven architecture

+
+ + {name} + + ); +}); + +const MessageBadge = memo(({ message }: { message: any }) => { + const data = message?.data || message; + const collection = message?.collection || 'events'; + const { Icon, color } = getMessageIcon(collection); + const id = data?.id || message?.id; + const name = data?.name || data?.id || id; + const version = data?.version; + + return ( + + + {name} + + ); +}); + +const ContainerBadge = memo(({ container, type }: { container: any; type: 'reads' | 'writes' }) => { + const data = container?.data || container; + const id = data?.id || container?.id; + const name = data?.name || id; + const version = data?.version; + const colorClass = type === 'reads' ? 'orange' : 'purple'; + + return ( + + + {name} + + ); +}); + +const ServiceCard = memo(({ service }: { service: any }) => { + const data = service?.data || service; + if (!data?.id) return null; + + const receives = data.receives || []; + const sends = data.sends || []; + const readsFrom = data.readsFrom || []; + const writesTo = data.writesTo || []; + const hasMessages = receives.length > 0 || sends.length > 0; + const hasContainers = readsFrom.length > 0 || writesTo.length > 0; + + return ( +
+ {/* Service Header */} + + + {data.summary &&

{data.summary}

} + + {/* Message Flow Diagram */} + {hasMessages && ( +
+ {/* Receives (Inbound) */} +
+
+ Inbound Messages + ({receives.length}) +
+ {receives.length > 0 ? ( +
+ {receives.slice(0, 4).map((msg: any, idx: number) => { + const msgId = msg?.data?.id || msg?.id; + return msgId ? : null; + })} + {receives.length > 4 &&

+{receives.length - 4} more

} +
+ ) : ( +

No incoming messages

+ )}
-
- - + {/* Service Icon (Center) */} +
+
+ +
-
-
-
- {filteredDomains.map((domain) => ( - s.data.id).join(','), - domainId: domain.data.id, - domainName: domain.data.name, - })} - className="group hover:bg-orange-100 border-2 border-orange-400/50 bg-yellow-50 rounded-lg shadow-sm hover:shadow-lg transition-all duration-200 overflow-hidden" - > -
-
-
- -

- {domain.data.name || domain.data.id} -

-
- - v{domain.data.version} - + {/* Sends (Outbound) */} +
+
+ Outbound Messages + ({sends.length}) +
+ {sends.length > 0 ? ( +
+ {sends.slice(0, 4).map((msg: any, idx: number) => { + const msgId = msg?.data?.id || msg?.id; + return msgId ? : null; + })} + {sends.length > 4 &&

+{sends.length - 4} more

}
+ ) : ( +

No outgoing messages

+ )} +
+
+ )} -

- {domain.data.summary || No summary available} -

- -
-
- -
-

{domain.data.domains?.length || 0} Subdomains

-
-
-
- -
-

{domain.data.services?.length || 0} Services

-
-
-
- -
-

- {(domain.sends?.length || 0) + (domain.receives?.length || 0)} Messages -

-
-
- {domain.data.entities && domain.data.entities.length > 0 && ( -
- -
-

{domain.data.entities?.length} Entities

-
-
- )} + {/* Container Relationships */} + {hasContainers && ( +
+ {/* Reads From */} + {readsFrom.length > 0 && ( +
+
+ + Reads from +
+
+ {readsFrom.slice(0, 3).map((container: any, idx: number) => { + const containerId = container?.data?.id || container?.id; + return containerId ? : null; + })} + {readsFrom.length > 3 && +{readsFrom.length - 3} more}
+
+ )} -
- {/* Subdomains and there services */} - {domain.data.domains?.slice(0, 2).map((subdomain: any) => ( -
-
-
- -

- {subdomain.data.name || subdomain.data.id} (Subdomain) -

-
- v{subdomain.data.version} -
- -
-
- -
-

{subdomain.data.services?.length || 0} Services

-
-
-
- -
-

- {(subdomain.sends?.length || 0) + (subdomain.receives?.length || 0)} Messages -

-
-
-
-
- ))} - - {/* Services and their messages */} - {domain.data.services?.slice(0, 2).map((service: any) => ( -
-
-
- -

{service.data.name || service.data.id}

-
- v{service.data.version} -
- -
-
-
- {service.data.receives?.slice(0, 3).map((message: any) => { - const { Icon, color } = getCollectionStyles(message.collection); - return ( -
-
- -
- {message.id} -
- ); - })} - {service.data.receives && service.data.receives.length > 3 && ( -
-

+ {service.data.receives.length - 3} more

-
- )} - {!service.data.receives?.length && ( -
-

No messages received

-
- )} -
-
- -
-
-
-
- -
-

{service.data.name || service.data.id}

-

v{service.data.version}

-
-
-
-
-
- -
-
- {service.data.sends?.slice(0, 3).map((message: any) => { - const { Icon, color } = getCollectionStyles(message.collection); - return ( -
-
- -
- - {message.id} -
- ); - })} - {service.data.sends && service.data.sends.length > 3 && ( -
-

+ {service.data.sends.length - 3} more

-
- )} - {!service.data.sends?.length && ( -
-

No messages sent

-
- )} -
-
-
- - {/* Container lists at the bottom */} - {((service.data.readsFrom && service.data.readsFrom.length > 0) || - (service.data.writesTo && service.data.writesTo.length > 0)) && ( -
- {/* Reads From */} - {service.data.readsFrom && service.data.readsFrom.length > 0 && ( -
-
- -

Reads from

-
-
- {service.data.readsFrom.slice(0, 3).map((container: any) => ( - - - {container.id} - - ))} - {service.data.readsFrom.length > 3 && ( - - + {service.data.readsFrom.length - 3} more - - )} -
-
- )} - - {/* Writes To */} - {service.data.writesTo && service.data.writesTo.length > 0 && ( -
-
- -

Writes to

-
-
- {service.data.writesTo.slice(0, 3).map((container: any) => ( - - - {container.id} - - ))} - {service.data.writesTo.length > 3 && ( - - + {service.data.writesTo.length - 3} more - - )} -
-
- )} -
- )} -
- ))} - {domain.data.domains && domain.data.domains.length > 2 && ( -
-
-
- -

+{domain.data.domains.length - 2} more subdomains

-
-
-
- )} - {domain.data.services && domain.data.services.length > 2 && ( -
-
-
- -

+{domain.data.services.length - 2} more services

-
-
-
- )} + {/* Writes To */} + {writesTo.length > 0 && ( +
+
+ + Writes to +
+
+ {writesTo.slice(0, 3).map((container: any, idx: number) => { + const containerId = container?.data?.id || container?.id; + return containerId ? ( + + ) : null; + })} + {writesTo.length > 3 && +{writesTo.length - 3} more}
+ )} +
+ )} +
+ ); +}); + +const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => { + const data = subdomain?.data || subdomain; + const [isCollapsed, setIsCollapsed] = useState(false); + + if (!data?.id) return null; + + const services = data.services || []; + const entities = data.entities || []; + + return ( +
+ {/* Subdomain Header */} + - {filteredDomains.length === 0 && ( -
-

No domains found matching your criteria

-
+ {!isCollapsed && ( + <> + {/* Subdomain Entities */} + {entities.length > 0 && ( +
+

Entities

+
+ {entities.map((entity: any) => { + const entityId = entity?.data?.id || entity?.id; + return entityId ? : null; + })} +
+
+ )} + + {/* Subdomain Services */} + {services.length > 0 && ( +
+

Services

+
+ {services.map((service: any) => { + const serviceId = service?.data?.id || service?.id; + // Ensure we pass the service down with its messages populated + return serviceId ? : null; + })} +
+
+ )} + + {entities.length === 0 && services.length === 0 && ( +

No entities or services in this subdomain

+ )} + )}
); +}); + +// ============================================ +// Main Component +// ============================================ + +export default function DomainGrid({ domain }: DomainGridProps) { + const data = domain?.data; + if (!data) return
No domain data
; + + const subdomains = data.domains || []; + const entities = data.entities || []; + const services = data.services || []; + + // Get services that are NOT in any subdomain + const subdomainServiceIds = useMemo( + () => + new Set( + subdomains.flatMap((sd: any) => { + const sdData = sd?.data || sd; + return (sdData?.services || []).map((s: any) => s?.data?.id || s?.id); + }) + ), + [subdomains] + ); + + const topLevelServices = useMemo( + () => + services.filter((s: any) => { + const sId = s?.data?.id || s?.id; + return sId && !subdomainServiceIds.has(sId); + }), + [services, subdomainServiceIds] + ); + + return ( +
+ {/* Domain Container - Yellow */} +
+ {/* Domain Header */} +
+
+ +

{data.name || data.id}

+ v{data.version} + Domain +
+ +
+ + {data.summary &&

{data.summary}

} + + {/* Domain Entities */} + {entities.length > 0 && ( +
+

Entities

+
+ {entities.map((entity: any) => { + const entityId = entity?.data?.id || entity?.id; + return entityId ? : null; + })} +
+
+ )} + + {/* Top-level Services (not in subdomains) */} + {topLevelServices.length > 0 && ( +
+

Services

+
+ {topLevelServices.map((service: any) => { + const serviceId = service?.data?.id || service?.id; + return serviceId ? : null; + })} +
+
+ )} + + {/* Subdomains - nested inside domain */} + {subdomains.length > 0 && ( +
+

Subdomains

+
+ {subdomains.map((subdomain: any) => { + const subdomainId = subdomain?.data?.id || subdomain?.id; + return subdomainId ? : null; + })} +
+
+ )} + + {/* Empty state */} + {entities.length === 0 && services.length === 0 && subdomains.length === 0 && ( +
+

This domain has no entities, services, or subdomains defined.

+
+ )} +
+
+ ); } diff --git a/eventcatalog/src/components/Grids/MessageGrid.tsx b/eventcatalog/src/components/Grids/MessageGrid.tsx index 97c77eab3..221891df0 100644 --- a/eventcatalog/src/components/Grids/MessageGrid.tsx +++ b/eventcatalog/src/components/Grids/MessageGrid.tsx @@ -1,197 +1,25 @@ -import { useState, useMemo, useEffect } from 'react'; -import { EnvelopeIcon, ChevronRightIcon, ServerIcon, CircleStackIcon } from '@heroicons/react/24/outline'; -import { RectangleGroupIcon } from '@heroicons/react/24/outline'; -import { buildUrl, buildUrlWithParams } from '@utils/url-builder'; +import { ServerIcon, CircleStackIcon } from '@heroicons/react/24/outline'; +import { buildUrl } from '@utils/url-builder'; import type { CollectionEntry } from 'astro:content'; -import type { CollectionMessageTypes } from '@types'; import { getCollectionStyles } from './utils'; -import { SearchBar, TypeFilters, Pagination } from './components'; -interface MessageGridProps { - messages: CollectionEntry[]; - containers?: CollectionEntry<'containers'>[]; - embeded: boolean; - isVisualiserEnabled: boolean; +interface MessageGridV2Props { + service: CollectionEntry<'services'>; + embeded?: boolean; } -interface GroupedMessages { - all?: CollectionEntry[]; - sends?: CollectionEntry[]; - receives?: CollectionEntry[]; -} - -export default function MessageGrid({ messages, embeded, containers, isVisualiserEnabled }: MessageGridProps) { - const [searchQuery, setSearchQuery] = useState(''); - const [urlParams, setUrlParams] = useState<{ - serviceId?: string; - serviceName?: string; - domainId?: string; - domainName?: string; - } | null>(null); - const [currentPage, setCurrentPage] = useState(1); - const [selectedTypes, setSelectedTypes] = useState([]); - const [producerConsumerFilter, setProducerConsumerFilter] = useState<'all' | 'no-producers' | 'no-consumers'>('all'); - const ITEMS_PER_PAGE = 15; - - // Effect to sync URL params with state - useEffect(() => { - const params = new URLSearchParams(window.location.search); - const serviceId = params.get('serviceId') || undefined; - const serviceName = params.get('serviceName') ? decodeURIComponent(params.get('serviceName')!) : undefined; - const domainId = params.get('domainId') || undefined; - const domainName = params.get('domainName') || undefined; - setUrlParams({ - serviceId, - serviceName, - domainId, - domainName, - }); - }, []); - - const filteredAndSortedMessages = useMemo(() => { - if (urlParams === null) return []; - - let result = [...messages]; - - // Filter by message type - if (selectedTypes.length > 0) { - result = result.filter((message) => selectedTypes.includes(message.collection)); - } - - // Apply producer/consumer filters - if (producerConsumerFilter === 'no-producers') { - result = result.filter((message) => !message.data.producers || message.data.producers.length === 0); - } else if (producerConsumerFilter === 'no-consumers') { - result = result.filter((message) => !message.data.consumers || message.data.consumers.length === 0); - } - - // Filter by service ID or name if present - if (urlParams.serviceId) { - result = result.filter( - (message) => - message.data.producers?.some( - (producer: any) => producer.id === urlParams.serviceId && !producer.id.includes('/versioned/') - ) || - message.data.consumers?.some( - (consumer: any) => consumer.id === urlParams.serviceId && !consumer.id.includes('/versioned/') - ) - ); - } - - // Filter by search query - if (searchQuery) { - const query = searchQuery.toLowerCase(); - result = result.filter( - (message) => - message.data.name?.toLowerCase().includes(query) || - message.data.summary?.toLowerCase().includes(query) || - message.data.producers?.some((producer: any) => producer.data.id?.toLowerCase().includes(query)) || - message.data.consumers?.some((consumer: any) => consumer.data.id?.toLowerCase().includes(query)) - ); - } - - // Sort by name by default - result.sort((a, b) => a.data.name.localeCompare(b.data.name)); - - return result; - }, [messages, searchQuery, urlParams, selectedTypes, producerConsumerFilter]); - - // Add totalPages calculation - const totalPages = useMemo(() => { - if (urlParams?.serviceId || urlParams?.domainId) return 1; - return Math.ceil(filteredAndSortedMessages.length / ITEMS_PER_PAGE); - }, [filteredAndSortedMessages.length, urlParams]); - - // Add paginatedMessages calculation - const paginatedMessages = useMemo(() => { - if (urlParams?.serviceId || urlParams?.domainId) { - return filteredAndSortedMessages; - } - - const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; - return filteredAndSortedMessages.slice(startIndex, startIndex + ITEMS_PER_PAGE); - }, [filteredAndSortedMessages, currentPage, urlParams]); - - // Reset pagination when search query or filters change - useEffect(() => { - setCurrentPage(1); - }, [searchQuery, selectedTypes]); - - // Group messages by sends/receives when a service is selected - const groupedMessages = useMemo(() => { - if (!urlParams?.serviceId) return { all: filteredAndSortedMessages }; - - const serviceIdentifier = urlParams.serviceId; - const sends = filteredAndSortedMessages.filter((message) => - message.data.producers?.some((producer: any) => producer.id === serviceIdentifier) - ); - const receives = filteredAndSortedMessages.filter((message) => - message.data.consumers?.some((consumer: any) => consumer.id === serviceIdentifier) - ); - - return { sends, receives }; - }, [filteredAndSortedMessages, urlParams]); - - // Get the containers that are referenced by the service - const serviceContainersReferenced = useMemo(() => { - if (!urlParams?.serviceId || !containers) return { writesTo: [], readsFrom: [] }; - return { - writesTo: containers.filter((container) => - container.data.servicesThatWriteToContainer?.some((service: any) => service.data.id === urlParams.serviceId) - ), - readsFrom: containers.filter((container) => - container.data.servicesThatReadFromContainer?.some((service: any) => service.data.id === urlParams.serviceId) - ), - }; - }, [containers, urlParams]); - - const renderTypeFilters = () => { - return ( -
-
- selectedTypes.includes(m.collection)).length} - /> -
- -
-
- - {producerConsumerFilter !== 'all' && ( - - )} -
-
-
- ); - }; +export default function MessageGridV2({ service, embeded = false }: MessageGridV2Props) { + const { sends = [], receives = [], writesTo = [], readsFrom = [] } = service.data; - const renderMessageGrid = (messages: CollectionEntry[]) => ( -
+ const renderMessageGrid = (messages: any[]) => ( + ); @@ -235,343 +40,181 @@ export default function MessageGrid({ messages, embeded, containers, isVisualise
); - const renderPaginationControls = () => { - if (totalPages <= 1 || urlParams?.serviceName || urlParams?.domainId) return null; - - return ( - - ); - }; - return ( -
- {/* Breadcrumb */} - - - {/* Title Section */} -
-
-
-
-

- {urlParams?.domainName ? `Messages in ${urlParams.serviceName}` : 'All Messages'} -

+
+ {/* Service Title */} + {/*
+ +

{service.data.name}

+ +
*/} + +
+ {/* Left Column - Receives Messages & Reads From Containers */} +
+ {/* Receives Messages Section */} +
+
+

+ + Inbound Messages ({receives.length}) +

-

- {urlParams?.domainName - ? `Browse messages in the ${urlParams.serviceName} service` - : 'Browse and discover messages in your event-driven architecture'} -

-
- -
- + {receives.length > 0 ? ( + renderMessageGrid(receives) + ) : ( +
+

No messages

+
+ )}
-
-
- -
- {/* Results count and top pagination */} -
- {renderTypeFilters()} - {renderPaginationControls()} -
-
- {filteredAndSortedMessages.length > 0 && ( -
- {urlParams?.domainName && ( - <> -
- -
- {isVisualiserEnabled && ( - - View in visualizer - - )} + {/* Reads From Containers */} + {readsFrom.length > 0 && ( +
+
+

+ + Reads from ({readsFrom.length}) +

+
+ + ))}
- + {/* Arrow from Reads From to Service */} +
+
+
+
+
)} +
-
- {urlParams?.serviceName ? ( - <> - {/*
*/} - {/* Service Title */} -
- -

{urlParams.serviceName}

-
- {isVisualiserEnabled && ( - - View in visualizer - - )} - - Read documentation - -
-
-
- {/* Left Column - Receives Messages & Reads From Containers */} -
- {/* Receives Messages Section */} -
-
-

- - Receives ({groupedMessages.receives?.length || 0}) -

-
- {groupedMessages.receives && groupedMessages.receives.length > 0 ? ( - renderMessageGrid(groupedMessages.receives) - ) : ( -
-

No messages

-
- )} -
- - {/* Reads From Containers - Only show if containers exist */} - {serviceContainersReferenced.readsFrom && serviceContainersReferenced.readsFrom.length > 0 && ( -
-
-

- - Reads from ({serviceContainersReferenced.readsFrom.length}) -

-
-
- {serviceContainersReferenced.readsFrom.map((container: CollectionEntry<'containers'>) => ( - -
- -

- {container.data.name} -

-
- {container.data.summary && ( -

{container.data.summary}

- )} -
- ))} -
- {/* Arrow from Reads From to Service */} -
-
-
-
-
- )} -
- - {/* Arrow from Receives to Service */} -
-
-
-
- - {/* Service Information (Center) */} -
-
- -

{urlParams.serviceName}

- - {/* Quick Stats Grid */} -
-
-
{groupedMessages.receives?.length || 0}
-
Receives
-
-
-
{groupedMessages.sends?.length || 0}
-
Sends
-
- {serviceContainersReferenced.readsFrom && serviceContainersReferenced.readsFrom.length > 0 && ( -
-
- {serviceContainersReferenced.readsFrom.length} -
-
Reads from
-
- )} - {serviceContainersReferenced.writesTo && serviceContainersReferenced.writesTo.length > 0 && ( -
-
- {serviceContainersReferenced.writesTo.length} -
-
Writes to
-
- )} -
-
-
+ {/* Arrow from Receives to Service */} +
+
+
+
- {/* Arrow from Service to Sends */} -
-
-
-
+ {/* Service Information (Center) */} +
+
+ +

{service.data.name}

+ + {/* Quick Stats Grid */} +
+
+
{receives.length}
+
Inbound Messages
+
+
+
{sends.length}
+
Outbound Messages
+
+ {readsFrom.length > 0 && ( +
+
{readsFrom.length}
+
Reads from
+
+ )} + {writesTo.length > 0 && ( +
+
{writesTo.length}
+
Writes to
+
+ )} +
+
+
- {/* Right Column - Sends Messages & Writes To Containers */} -
- {/* Sends Messages Section */} -
-
-

- - Sends ({groupedMessages.sends?.length || 0}) -

-
- {groupedMessages.sends && groupedMessages.sends.length > 0 ? ( - renderMessageGrid(groupedMessages.sends) - ) : ( -
-

No messages

-
- )} -
+ {/* Arrow from Service to Sends */} +
+
+
+
- {/* Writes To Containers - Only show if containers exist */} - {serviceContainersReferenced.writesTo && serviceContainersReferenced.writesTo.length > 0 && ( -
- {/* Arrow from Service to Writes To */} -
-
-
-
-
-

- - Writes to ({serviceContainersReferenced.writesTo.length}) -

-
-
- {serviceContainersReferenced.writesTo.map((container: CollectionEntry<'containers'>) => ( - -
- -

- {container.data.name} -

-
- {container.data.summary && ( -

{container.data.summary}

- )} -
- ))} -
-
- )} -
-
- + {/* Right Column - Sends Messages & Writes To Containers */} +
+ {/* Sends Messages Section */} +
+
+

+ + Outbound Messages ({sends.length}) +

+
+ {sends.length > 0 ? ( + renderMessageGrid(sends) ) : ( - <> - {renderMessageGrid(paginatedMessages)} -
{renderPaginationControls()}
- +
+

No messages

+
)}
-
- )} - {filteredAndSortedMessages.length === 0 && ( -
-

No messages found matching your criteria

+ {/* Writes To Containers */} + {writesTo.length > 0 && ( +
+ {/* Arrow from Service to Writes To */} +
+
+
+
+
+

+ + Writes to ({writesTo.length}) +

+
+ +
+ )}
- )} +
); } diff --git a/eventcatalog/src/components/Grids/ServiceGrid.tsx b/eventcatalog/src/components/Grids/ServiceGrid.tsx deleted file mode 100644 index 85f3b649c..000000000 --- a/eventcatalog/src/components/Grids/ServiceGrid.tsx +++ /dev/null @@ -1,540 +0,0 @@ -import { useState, useMemo, useEffect, memo } from 'react'; -import { ServerIcon, ChevronRightIcon, Squares2X2Icon, QueueListIcon, CircleStackIcon } from '@heroicons/react/24/outline'; -import { RectangleGroupIcon } from '@heroicons/react/24/outline'; -import { buildUrl, buildUrlWithParams } from '@utils/url-builder'; -import type { CollectionEntry } from 'astro:content'; -import type { CollectionMessageTypes } from '@types'; -import { getCollectionStyles } from './utils'; -import { SearchBar, TypeFilters, Pagination } from './components'; -import type { ExtendedDomain } from './DomainGrid'; -import { BoxIcon } from 'lucide-react'; - -// Message component for reuse -const Message = memo(({ message, collection }: { message: any; collection: string }) => { - const { Icon, color } = getCollectionStyles(message.collection); - return ( - -
- -
- {message.data.name} -
- ); -}); - -// Messages Container component -const MessagesContainer = memo( - ({ messages, type, selectedTypes }: { messages: any[]; type: 'receives' | 'sends'; selectedTypes: string[] }) => { - const bgColor = type === 'receives' ? 'blue' : 'green'; - const MAX_MESSAGES_DISPLAYED = 4; - - const filteredMessages = messages?.filter( - (message: any) => selectedTypes.length === 0 || selectedTypes.includes(message.collection) - ); - - const messagesToShow = filteredMessages?.slice(0, MAX_MESSAGES_DISPLAYED); - const remainingMessagesCount = filteredMessages ? filteredMessages.length - MAX_MESSAGES_DISPLAYED : 0; - - return ( -
-
- {messagesToShow?.map((message: any) => ( - - ))} - {remainingMessagesCount > 0 && ( -
-

+ {remainingMessagesCount} more

-
- )} - {(!messages?.length || - (selectedTypes.length > 0 && !messages?.some((message: any) => selectedTypes.includes(message.collection)))) && ( -
-

- {selectedTypes.length > 0 - ? `Service does not ${type} ${selectedTypes.join(' or ')}` - : `Service does not ${type} any messages`} -

-
- )} -
-
- ); - } -); - -// Service Card component -const ServiceCard = memo(({ service, urlParams, selectedTypes }: { service: any; urlParams: any; selectedTypes: string[] }) => { - return ( - -
-
-
- -

- {service.data.name || service.data.id} (v{service.data.version}) -

-
-
- - {service.data.summary &&

{service.data.summary}

} - - {!urlParams?.serviceName && ( -
- - -
-
-
-
- -
-

{service.data.name || service.data.id}

-

v{service.data.version}

-
-
-
-
-
- - -
- )} - - {/* Container lists at the bottom */} - {((service.data.readsFrom && service.data.readsFrom.length > 0) || - (service.data.writesTo && service.data.writesTo.length > 0)) && ( -
- {/* Reads From */} - {service.data.readsFrom && service.data.readsFrom.length > 0 && ( -
-
- -

Reads from

-
-
- {service.data.readsFrom.slice(0, 3).map((container: any) => ( - - - {container.data.name} - - ))} - {service.data.readsFrom.length > 3 && ( - - + {service.data.readsFrom.length - 3} more - - )} -
-
- )} - - {/* Writes To */} - {service.data.writesTo && service.data.writesTo.length > 0 && ( -
-
- -

Writes to

-
-
- {service.data.writesTo.slice(0, 3).map((container: any) => ( - - - {container.data.name} - - ))} - {service.data.writesTo.length > 3 && ( - - + {service.data.writesTo.length - 3} more - - )} -
-
- )} -
- )} -
- - ); -}); - -// Domain Section component -const DomainSection = memo( - ({ - domain, - services, - urlParams, - selectedTypes, - isMultiColumn, - isVisualiserEnabled, - }: { - domain: any; - services: any[]; - urlParams: any; - selectedTypes: string[]; - isMultiColumn: boolean; - isVisualiserEnabled: boolean; - }) => { - const subdomains = domain.data.domains || []; - const allSubDomainServices = subdomains.map((subdomain: any) => subdomain.data.services || []).flat(); - - const servicesWithoutSubdomains = services.filter((service) => { - return !allSubDomainServices.some((s: any) => s.id === service.data.id); - }); - - return ( -
- {servicesWithoutSubdomains.length > 0 && ( -
- {servicesWithoutSubdomains.map((service) => ( - - ))} -
- )} - - {subdomains.map((subdomainRef: any) => { - const subdomain = domain.data.domains?.find((d: any) => d.data.id === subdomainRef.data.id); - if (!subdomain) return null; - - const subdomainServices = services.filter((service) => - subdomain.data.services?.some((s: any) => s.id === service.data.id) - ); - - if (subdomainServices.length === 0) return null; - - return ( -
-
-
- -

{subdomain.data.name} (Subdomain)

-
-
- {isVisualiserEnabled && ( - - View in visualizer - - )} - - Read documentation - -
-
- - {/* Entities */} - {subdomain.data.entities && subdomain.data.entities.length > 0 && ( -
-
- -

Entities

-
-
- {subdomain.data.entities.map((entity: any) => ( - - - {entity.id} - - ))} -
-
- )} - -
- {subdomainServices.map((service) => ( - - ))} -
-
- ); - })} -
- ); - } -); - -interface ServiceGridProps { - services: CollectionEntry<'services'>[]; - domains: ExtendedDomain[]; - embeded: boolean; - isVisualiserEnabled: boolean; -} - -// Main ServiceGrid component -export default function ServiceGrid({ services, domains, embeded, isVisualiserEnabled }: ServiceGridProps) { - const [searchQuery, setSearchQuery] = useState(''); - const [currentPage, setCurrentPage] = useState(1); - const [selectedTypes, setSelectedTypes] = useState([]); - const [isMultiColumn, setIsMultiColumn] = useState(false); - const ITEMS_PER_PAGE = 16; - const [urlParams, setUrlParams] = useState<{ - serviceIds?: string[]; - domainId?: string; - domainName?: string; - serviceName?: string; - } | null>(null); - - useEffect(() => { - const params = new URLSearchParams(window.location.search); - setUrlParams({ - serviceIds: params.get('serviceIds')?.split(',').filter(Boolean), - domainId: params.get('domainId') || undefined, - domainName: params.get('domainName') || undefined, - serviceName: params.get('serviceName') || undefined, - }); - }, []); - - useEffect(() => { - if (typeof window !== 'undefined') { - const saved = localStorage.getItem('EventCatalog:ServiceColumnLayout'); - if (saved !== null) { - setIsMultiColumn(saved === 'multi'); - } - } - }, []); - - const toggleColumnLayout = () => { - const newValue = !isMultiColumn; - setIsMultiColumn(newValue); - if (typeof window !== 'undefined') { - localStorage.setItem('EventCatalog:ServiceColumnLayout', newValue ? 'multi' : 'single'); - } - }; - - const filteredAndSortedServices = useMemo(() => { - if (urlParams === null) return []; - - let result = [...services]; - - if (urlParams.serviceIds?.length) { - result = result.filter( - (service) => urlParams.serviceIds?.includes(service.data.id) && !service.data.id.includes('/versioned/') - ); - } - - if (searchQuery) { - const query = searchQuery.toLowerCase(); - result = result.filter( - (service) => - service.data.name?.toLowerCase().includes(query) || - service.data.summary?.toLowerCase().includes(query) || - service.data.sends?.some((message: any) => message.data.name.toLowerCase().includes(query)) || - service.data.receives?.some((message: any) => message.data.name.toLowerCase().includes(query)) - ); - } - - if (selectedTypes.length > 0) { - result = result.filter((service) => { - const hasMatchingSends = service.data.sends?.some((message: any) => selectedTypes.includes(message.collection)); - const hasMatchingReceives = service.data.receives?.some((message: any) => selectedTypes.includes(message.collection)); - return hasMatchingSends || hasMatchingReceives; - }); - } - - result.sort((a, b) => (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id)); - return result; - }, [services, searchQuery, urlParams, selectedTypes]); - - const paginatedServices = useMemo(() => { - if (urlParams?.domainId || urlParams?.serviceIds?.length) { - return filteredAndSortedServices; - } - const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; - return filteredAndSortedServices.slice(startIndex, startIndex + ITEMS_PER_PAGE); - }, [filteredAndSortedServices, currentPage, urlParams]); - - const totalPages = useMemo(() => { - if (urlParams?.domainId || urlParams?.serviceIds?.length) return 1; - return Math.ceil(filteredAndSortedServices.length / ITEMS_PER_PAGE); - }, [filteredAndSortedServices.length, urlParams]); - - useEffect(() => { - setCurrentPage(1); - }, [searchQuery, selectedTypes]); - - return ( -
- {/* Breadcrumb */} - - - {/* Title Section */} -
-
-
-
-

- {urlParams?.domainId ? `${urlParams.domainName} Architecture` : 'All Services'} -

-
-

- {urlParams?.domainId - ? `Browse services and messages in the ${urlParams.domainId} domain` - : 'Browse and discover services in your event-driven architecture'} -

-
- -
- - -
-
-
- -
- {/* Results count and pagination */} -
- -
- {urlParams?.domainId || urlParams?.serviceIds?.length ? ( - - Showing {filteredAndSortedServices.length} services in the {urlParams.domainId} domain - - ) : ( - - Showing {(currentPage - 1) * ITEMS_PER_PAGE + 1} to{' '} - {Math.min(currentPage * ITEMS_PER_PAGE, filteredAndSortedServices.length)} of {filteredAndSortedServices.length}{' '} - services - - )} -
- {!(urlParams?.domainId || urlParams?.serviceIds?.length) && ( - - )} -
-
- - {filteredAndSortedServices.length > 0 && ( -
- {urlParams?.domainName ? ( - domains - .filter((domain: ExtendedDomain) => domain.data.id === urlParams.domainId) - .map((domain: ExtendedDomain) => ( - - )) - ) : ( -
- {paginatedServices.map((service) => ( - - ))} -
- )} -
- )} - - {filteredAndSortedServices.length === 0 && ( -
-

- {selectedTypes.length > 0 - ? `No services found that ${selectedTypes.length > 1 ? 'handle' : 'handles'} ${selectedTypes.join(' or ')} messages` - : 'No services found matching your criteria'} -

-
- )} - - {/* Bottom pagination */} - {!(urlParams?.domainId || urlParams?.serviceIds?.length) && ( -
- -
- )} -
- ); -} diff --git a/eventcatalog/src/components/Header.astro b/eventcatalog/src/components/Header.astro index eb0519b4c..3dcb98801 100644 --- a/eventcatalog/src/components/Header.astro +++ b/eventcatalog/src/components/Header.astro @@ -2,10 +2,11 @@ import catalog from '@utils/eventcatalog-config/catalog'; import Search from '@components/Search/Search.astro'; import { buildUrl } from '@utils/url-builder'; -import { showEventCatalogBranding, showCustomBranding } from '@utils/feature'; +import { showEventCatalogBranding, showCustomBranding, isEventCatalogChatEnabled } from '@utils/feature'; import { getSession } from 'auth-astro/server'; import { isAuthEnabled, isSSR } from '@utils/feature'; import { EnvironmentDropdown } from './EnvironmentDropdown'; +import ChatPanelButton from './ChatPanel/ChatPanelButton'; let session = null; if (isAuthEnabled()) { @@ -13,7 +14,7 @@ if (isAuthEnabled()) { } const logo = { - src: ('/' + (catalog?.logo?.src || 'logo.png')).replace(/^\/+/, '/'), + src: ('/' + (catalog?.logo?.src || '')).replace(/^\/+/, '/'), alt: catalog?.logo?.alt || 'Event Catalog', text: catalog?.logo?.text || 'EventCatalog', }; @@ -23,20 +24,30 @@ const repositoryUrl = catalog?.repositoryUrl || 'https://github.com/event-catalo