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 9df4b958f..000000000 Binary files a/eventcatalog/public/logo_old.png and /dev/null differ 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