@eventcatalog/core 4.7.11 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,7 +36,7 @@ module.exports = __toCommonJS(analytics_exports);
36
36
  var import_os = __toESM(require("os"), 1);
37
37
 
38
38
  // package.json
39
- var version = "4.7.11";
39
+ var version = "4.8.0";
40
40
 
41
41
  // src/constants.ts
42
42
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "../chunk-FNV7TXII.js";
4
- import "../chunk-6GGS5ZKX.js";
3
+ } from "../chunk-EDMVOV4H.js";
4
+ import "../chunk-XDKU675D.js";
5
5
  export {
6
6
  raiseEvent
7
7
  };
@@ -140,7 +140,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
140
140
  var import_os = __toESM(require("os"), 1);
141
141
 
142
142
  // package.json
143
- var version = "4.7.11";
143
+ var version = "4.8.0";
144
144
 
145
145
  // src/constants.ts
146
146
  var VERSION = version;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  log_build_default
3
- } from "../chunk-SQWXIH42.js";
4
- import "../chunk-FNV7TXII.js";
3
+ } from "../chunk-TRDXMFBW.js";
4
+ import "../chunk-EDMVOV4H.js";
5
5
  import "../chunk-K2XIENVT.js";
6
- import "../chunk-6GGS5ZKX.js";
6
+ import "../chunk-XDKU675D.js";
7
7
  import "../chunk-6QENHZZP.js";
8
8
  export {
9
9
  log_build_default as default
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-6GGS5ZKX.js";
3
+ } from "./chunk-XDKU675D.js";
4
4
 
5
5
  // src/analytics/analytics.js
6
6
  import os from "os";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-45OUURWB.js";
3
+ } from "./chunk-OGZMUG75.js";
4
4
  import {
5
5
  cleanup,
6
6
  getEventCatalogConfigFile
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-6GGS5ZKX.js";
3
+ } from "./chunk-XDKU675D.js";
4
4
 
5
5
  // src/utils/cli-logger.ts
6
6
  import pc from "picocolors";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "./chunk-FNV7TXII.js";
3
+ } from "./chunk-EDMVOV4H.js";
4
4
  import {
5
5
  countResources,
6
6
  serializeCounts
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "4.7.11";
2
+ var version = "4.8.0";
3
3
 
4
4
  // src/constants.ts
5
5
  var VERSION = version;
@@ -25,7 +25,7 @@ __export(constants_exports, {
25
25
  module.exports = __toCommonJS(constants_exports);
26
26
 
27
27
  // package.json
28
- var version = "4.7.11";
28
+ var version = "4.8.0";
29
29
 
30
30
  // src/constants.ts
31
31
  var VERSION = version;
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-6GGS5ZKX.js";
3
+ } from "./chunk-XDKU675D.js";
4
4
  export {
5
5
  VERSION
6
6
  };
@@ -144,7 +144,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
144
144
  var import_picocolors = __toESM(require("picocolors"), 1);
145
145
 
146
146
  // package.json
147
- var version = "4.7.11";
147
+ var version = "4.8.0";
148
148
 
149
149
  // src/constants.ts
150
150
  var VERSION = version;
@@ -4,8 +4,8 @@ import {
4
4
  import "./chunk-CA4U2JP7.js";
5
5
  import {
6
6
  log_build_default
7
- } from "./chunk-SQWXIH42.js";
8
- import "./chunk-FNV7TXII.js";
7
+ } from "./chunk-TRDXMFBW.js";
8
+ import "./chunk-EDMVOV4H.js";
9
9
  import "./chunk-K2XIENVT.js";
10
10
  import {
11
11
  FederationConflictError,
@@ -31,10 +31,10 @@ import {
31
31
  } from "./chunk-B7HCX5HM.js";
32
32
  import {
33
33
  generate
34
- } from "./chunk-RD5TL6VE.js";
34
+ } from "./chunk-JCJYBRLL.js";
35
35
  import {
36
36
  logger
37
- } from "./chunk-45OUURWB.js";
37
+ } from "./chunk-OGZMUG75.js";
38
38
  import {
39
39
  resolve_catalog_dependencies_default
40
40
  } from "./chunk-LHR4G2UO.js";
@@ -54,7 +54,7 @@ import {
54
54
  import "./chunk-W5JQON7Z.js";
55
55
  import {
56
56
  VERSION
57
- } from "./chunk-6GGS5ZKX.js";
57
+ } from "./chunk-XDKU675D.js";
58
58
  import {
59
59
  linkCoreNodeModules,
60
60
  resolveInstalledCoreNodeModules
package/dist/generate.cjs CHANGED
@@ -108,7 +108,7 @@ var getEventCatalogConfigFile = async (projectDirectory) => {
108
108
  var import_picocolors = __toESM(require("picocolors"), 1);
109
109
 
110
110
  // package.json
111
- var version = "4.7.11";
111
+ var version = "4.8.0";
112
112
 
113
113
  // src/constants.ts
114
114
  var VERSION = version;
package/dist/generate.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  generate
3
- } from "./chunk-RD5TL6VE.js";
4
- import "./chunk-45OUURWB.js";
5
- import "./chunk-6GGS5ZKX.js";
3
+ } from "./chunk-JCJYBRLL.js";
4
+ import "./chunk-OGZMUG75.js";
5
+ import "./chunk-XDKU675D.js";
6
6
  import "./chunk-6QENHZZP.js";
7
7
  export {
8
8
  generate
@@ -36,7 +36,7 @@ module.exports = __toCommonJS(cli_logger_exports);
36
36
  var import_picocolors = __toESM(require("picocolors"), 1);
37
37
 
38
38
  // package.json
39
- var version = "4.7.11";
39
+ var version = "4.8.0";
40
40
 
41
41
  // src/constants.ts
42
42
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  logger
3
- } from "../chunk-45OUURWB.js";
4
- import "../chunk-6GGS5ZKX.js";
3
+ } from "../chunk-OGZMUG75.js";
4
+ import "../chunk-XDKU675D.js";
5
5
  export {
6
6
  logger
7
7
  };
@@ -25,15 +25,15 @@ const flow = flowCollection[0];
25
25
 
26
26
  const { nodes, edges } = await getNodesAndEdges({
27
27
  id: id,
28
- version: flow.data.version,
28
+ version: flow?.data.version,
29
29
  mode: mode,
30
30
  });
31
31
 
32
32
  // Load and apply saved layout if it exists
33
- const resourceKey = buildResourceKey('flows', id, flow.data.version);
33
+ const resourceKey = buildResourceKey('flows', id, flow?.data.version);
34
34
  const savedLayout = await loadSavedLayout(resourceKey);
35
35
  const nodesWithLayout = applyLayoutToNodes(nodes, savedLayout);
36
- const portalId = `${id}-${flow.data.version}-flow-${randomUUID()}-portal`;
36
+ const portalId = `${id}-${flow?.data.version}-flow-${randomUUID()}-portal`;
37
37
  ---
38
38
 
39
39
  {
@@ -439,9 +439,10 @@ export default function NestedSideBar() {
439
439
  if (!data || roots.length === 0) {
440
440
  return (
441
441
  <aside className="flex h-full min-h-0 w-full flex-1 flex-col bg-[rgb(var(--ec-rail-bg))] font-sans">
442
- {/* Search skeleton */}
443
- <div className="px-4 py-3 border-b border-[rgb(var(--ec-content-border))] bg-[rgb(var(--ec-rail-bg))]">
444
- <div className="h-10 bg-[rgb(var(--ec-content-hover))] rounded-xl animate-pulse" />
442
+ <div className="flex h-[60px] items-center px-6 bg-[rgb(var(--ec-rail-bg)/0.98)] backdrop-blur-sm border-b border-[rgb(var(--ec-content-border))] sticky top-0 z-10">
443
+ <span className="text-[0.65rem] font-semibold uppercase tracking-[0.18em] text-[rgb(var(--ec-sidebar-text)/0.5)] truncate">
444
+ All resources
445
+ </span>
445
446
  </div>
446
447
  {/* Content skeleton */}
447
448
  <div className="p-4 space-y-3">
@@ -1,5 +1,6 @@
1
1
  import type { APIRoute } from 'astro';
2
2
  import { getNestedSideBarData } from '@stores/sidebar-store/state';
3
+ import { getSearchIndexNodeEntries } from '@stores/sidebar-store/search-index';
3
4
 
4
5
  const isDev = import.meta.env.DEV;
5
6
 
@@ -18,7 +19,7 @@ const SKIPPED_PREFIXES = ['list:'];
18
19
  export const GET: APIRoute = async () => {
19
20
  const sidebarData = await getNestedSideBarData();
20
21
 
21
- const items = Object.entries(sidebarData.nodes).reduce<SearchIndexItemCompact[]>((acc, [key, node]) => {
22
+ const items = getSearchIndexNodeEntries(sidebarData.nodes).reduce<SearchIndexItemCompact[]>((acc, [key, node]) => {
22
23
  if (typeof node === 'string') return acc;
23
24
  if (SKIPPED_PREFIXES.some((prefix) => key.startsWith(prefix))) return acc;
24
25
  if (!node.title) return acc;
@@ -0,0 +1,17 @@
1
+ import type { NavigationData, NavNode } from './builders/shared';
2
+
3
+ const MESSAGE_NODE_PREFIXES = ['event:', 'command:', 'query:'];
4
+
5
+ export const getSearchIndexNodeEntries = (nodes: NavigationData['nodes']): [string, NavNode | string][] => {
6
+ const currentMessageTargets = new Set(
7
+ Object.entries(nodes).flatMap(([key, node]) => {
8
+ const isMessageAlias = MESSAGE_NODE_PREFIXES.some((prefix) => key.startsWith(prefix));
9
+ return typeof node === 'string' && isMessageAlias && node.startsWith(`${key}:`) ? [node] : [];
10
+ })
11
+ );
12
+
13
+ return Object.entries(nodes).filter(([key]) => {
14
+ const isMessageNode = MESSAGE_NODE_PREFIXES.some((prefix) => key.startsWith(prefix));
15
+ return !isMessageNode || currentMessageTargets.has(key);
16
+ });
17
+ };
@@ -281,7 +281,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
281
281
  systems,
282
282
  agents,
283
283
  services,
284
- { events, commands, queries },
284
+ { events: allEvents, commands: allCommands, queries: allQueries },
285
285
  containers,
286
286
  flows,
287
287
  users,
@@ -301,7 +301,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
301
301
  getSystems({ getAllVersions: false }),
302
302
  getAgents({ getAllVersions: false }),
303
303
  getServices({ getAllVersions: false }),
304
- getMessages({ getAllVersions: false }),
304
+ getMessages({ getAllVersions: true }),
305
305
  getContainers({ getAllVersions: false }),
306
306
  getFlows({ getAllVersions: false }),
307
307
  getUsers(),
@@ -322,7 +322,10 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
322
322
  const allSubDomainIds = new Set(domains.flatMap((d) => (d.data.domains || []).map((sd: any) => sd.data.id)));
323
323
  const rootDomains = domains.filter((d) => !allSubDomainIds.has(d.data.id));
324
324
 
325
- const messages = [...events, ...commands, ...queries];
325
+ const events = allEvents.filter((event) => event.data.version === event.data.latestVersion);
326
+ const commands = allCommands.filter((command) => command.data.version === command.data.latestVersion);
327
+ const queries = allQueries.filter((query) => query.data.version === query.data.latestVersion);
328
+ const messages = [...allEvents, ...allCommands, ...allQueries];
326
329
 
327
330
  const context = {
328
331
  agents,
@@ -595,7 +598,12 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
595
598
  }
596
599
  }
597
600
 
598
- const flowRefsByMessage = buildFlowReferencesByMessage({ flows, events, commands, queries });
601
+ const flowRefsByMessage = buildFlowReferencesByMessage({
602
+ flows,
603
+ events: allEvents,
604
+ commands: allCommands,
605
+ queries: allQueries,
606
+ });
599
607
 
600
608
  const messageNodes = messagesWithOwners.reduce(
601
609
  (acc, { message, owners }) => {
@@ -1,6 +1,5 @@
1
1
  import { getCollection } from 'astro:content';
2
2
  import type { CollectionEntry } from 'astro:content';
3
- import semver from 'semver';
4
3
  import type { CollectionMessageTypes, CollectionTypes } from '@types';
5
4
  import { getDomains, getDomainsForService } from './domains';
6
5
  import {
@@ -1,7 +1,7 @@
1
1
  import type { CollectionTypes } from '@types';
2
2
  import type { CollectionEntry } from 'astro:content';
3
- import semver, { coerce, compare, satisfies as satisfiesRange } from 'semver';
4
3
  import path from 'node:path';
4
+ import { compareVersions, toComparableVersion, versionSatisfiesRange } from './version-compare';
5
5
 
6
6
  // --- FILE PATH HELPERS ---
7
7
 
@@ -95,12 +95,11 @@ export { isSameVersion } from './version-compare';
95
95
  * Sorts versioned items. Latest version first.
96
96
  */
97
97
  export function sortVersioned<T>(versioned: T[], versionExtractor: (e: T) => string): T[] {
98
- // try to coerce semver versions from input version
99
- const semverVersions = versioned.map((v) => ({ original: v, semver: coerce(versionExtractor(v)) }));
98
+ const versions = versioned.map((original) => ({ original, version: versionExtractor(original) }));
100
99
 
101
100
  // if all versions are semver'ish, use semver to sort them
102
- if (semverVersions.every((v) => v.semver != null)) {
103
- const sorted = semverVersions.sort((a, b) => compare(b.semver!, a.semver!));
101
+ if (versions.every(({ version }) => toComparableVersion(version) !== undefined)) {
102
+ const sorted = versions.sort((a, b) => compareVersions(b.version, a.version)!);
104
103
 
105
104
  return sorted.map((v) => v.original);
106
105
  } else {
@@ -137,9 +136,8 @@ export function sortStringVersions(versions: string[]) {
137
136
  * @returns {boolean} Returns true if the version satisfies the range.
138
137
  */
139
138
  export const satisfies = (version: string, range: string): boolean => {
140
- const coercedVersion = coerce(version);
141
- if (!coercedVersion) return false;
142
- return satisfiesRange(coercedVersion, range);
139
+ if (version === range) return true;
140
+ return versionSatisfiesRange(version, range);
143
141
  };
144
142
 
145
143
  export const getItemsFromCollectionByIdAndSemverOrLatest = <T extends { data: { id: string; version: string } }>(
@@ -150,6 +148,9 @@ export const getItemsFromCollectionByIdAndSemverOrLatest = <T extends { data: {
150
148
  const filteredCollection = collection.filter((c) => c.data.id == id);
151
149
 
152
150
  if (version && version != 'latest') {
151
+ const exactMatches = filteredCollection.filter((item) => item.data.version === version);
152
+ if (exactMatches.length > 0) return exactMatches;
153
+
153
154
  return filteredCollection.filter((c) => satisfies(c.data.version, version));
154
155
  }
155
156
 
@@ -282,12 +283,10 @@ export const createVersionedMap = <T extends { data: { id: string; version?: str
282
283
 
283
284
  // Sort every entry so index [0] is always the latest version
284
285
  for (const [key, list] of map.entries()) {
285
- list.sort((a, b) => {
286
- // specific version sorting logic (fallback to string compare if not valid semver)
287
- const vA = a.data.version || '0.0.0';
288
- const vB = b.data.version || '0.0.0';
289
- return semver.valid(vB) && semver.valid(vA) ? semver.rcompare(vA, vB) : vB.localeCompare(vA);
290
- });
286
+ map.set(
287
+ key,
288
+ sortVersioned(list, (item) => item.data.version || '0.0.0')
289
+ );
291
290
  }
292
291
  return map;
293
292
  };
@@ -319,11 +318,7 @@ export const findInMap = <T extends { data: { version?: string } }>(
319
318
  if (exactMatch) return exactMatch;
320
319
 
321
320
  // Try semver match if not exact
322
- if (semver.validRange(version)) {
323
- return items.find((i) => semver.satisfies(i.data.version || '0.0.0', version));
324
- }
325
-
326
- return undefined;
321
+ return items.find((i) => satisfies(i.data.version || '0.0.0', version));
327
322
  };
328
323
 
329
324
  /**
@@ -342,13 +337,7 @@ export const versionMatches = (version: string, rangePattern: string): boolean =
342
337
  if (version === rangePattern) return true;
343
338
 
344
339
  // Try semver range matching
345
- try {
346
- if (semver.validRange(rangePattern)) {
347
- return semver.satisfies(version, rangePattern);
348
- }
349
- } catch (error) {
350
- // Invalid semver, fall through
351
- }
340
+ if (satisfies(version, rangePattern)) return true;
352
341
 
353
342
  // Handle x-patterns like 1.x, 1.2.x
354
343
  if (rangePattern.includes('.x')) {
@@ -1,12 +1,41 @@
1
- import { coerce, eq } from 'semver';
1
+ import { coerce, compare, satisfies, valid, validRange } from 'semver';
2
+
3
+ // Keep in sync with packages/sdk/src/internal/versions.ts
4
+ const NUMERIC_VERSION_PATTERN = /^[vV]?\d+(?:\.\d+){0,2}$/;
5
+
6
+ export function toComparableVersion(version: string | undefined) {
7
+ if (!version) return undefined;
8
+
9
+ const strictVersion = valid(version);
10
+ if (strictVersion) return strictVersion;
11
+
12
+ if (!NUMERIC_VERSION_PATTERN.test(version)) return undefined;
13
+
14
+ return coerce(version) ?? undefined;
15
+ }
16
+
17
+ export function compareVersions(v1: string | undefined, v2: string | undefined): number | undefined {
18
+ const semverV1 = toComparableVersion(v1);
19
+ const semverV2 = toComparableVersion(v2);
20
+
21
+ if (!semverV1 || !semverV2) return undefined;
22
+
23
+ return compare(semverV1, semverV2);
24
+ }
25
+
26
+ export function versionSatisfiesRange(version: string, range: string) {
27
+ const comparableVersion = toComparableVersion(version);
28
+ const comparableRange = validRange(range) ?? validRange(range.replace(/V(?=\d)/g, 'v'));
29
+
30
+ if (!comparableVersion || !comparableRange) return false;
31
+
32
+ return satisfies(comparableVersion, comparableRange);
33
+ }
2
34
 
3
35
  export function isSameVersion(v1: string | undefined, v2: string | undefined) {
4
- const semverV1 = coerce(v1);
5
- const semverV2 = coerce(v2);
36
+ const comparison = compareVersions(v1, v2);
6
37
 
7
- if (semverV1 != null && semverV2 != null) {
8
- return eq(semverV1, semverV2);
9
- }
38
+ if (comparison !== undefined) return comparison === 0;
10
39
 
11
40
  return v1 === v2;
12
41
  }
@@ -1,31 +1,11 @@
1
1
  import type { CollectionTypes } from '@types';
2
2
  import type { CollectionEntry } from 'astro:content';
3
- import { coerce, compare, satisfies, validRange } from 'semver';
3
+ import { getItemsFromCollectionByIdAndSemverOrLatest } from './util';
4
4
 
5
5
  export const getVersionFromCollection = (
6
6
  collection: CollectionEntry<CollectionTypes>[],
7
7
  id: string,
8
8
  version?: string
9
9
  ): CollectionEntry<CollectionTypes>[] => {
10
- const data = collection;
11
- const semverRange = validRange(version);
12
-
13
- if (semverRange) {
14
- return data.filter((msg) => msg.data.id == id).filter((msg) => satisfies(msg.data.version, semverRange));
15
- }
16
-
17
- const filteredEvents = data.filter((event) => event.data.id === id);
18
-
19
- // Order by semver
20
- const sorted = filteredEvents.sort((a, b) => {
21
- const semverA = coerce(a.data.version);
22
- const semverB = coerce(b.data.version);
23
- if (semverA && semverB) {
24
- return compare(semverA, semverB);
25
- }
26
- return a.data.version.localeCompare(b.data.version);
27
- });
28
-
29
- // latest version
30
- return [sorted[sorted.length - 1]];
10
+ return getItemsFromCollectionByIdAndSemverOrLatest(collection, id, version);
31
11
  };
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "SEE LICENSE IN LICENSE",
9
9
  "type": "module",
10
- "version": "4.7.11",
10
+ "version": "4.8.0",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -119,9 +119,9 @@
119
119
  "update-notifier": "^7.3.1",
120
120
  "uuid": "^11.1.1",
121
121
  "zod": "^4.3.6",
122
- "@eventcatalog/linter": "1.1.14",
123
- "@eventcatalog/visualiser": "^4.1.3",
124
- "@eventcatalog/sdk": "2.27.4"
122
+ "@eventcatalog/sdk": "2.28.0",
123
+ "@eventcatalog/linter": "1.1.15",
124
+ "@eventcatalog/visualiser": "^4.1.3"
125
125
  },
126
126
  "devDependencies": {
127
127
  "@astrojs/check": "^0.9.10",
@@ -1,349 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useCallback, useMemo } from 'react';
4
- import {
5
- Search,
6
- X,
7
- SlidersHorizontal,
8
- ChevronRight,
9
- Boxes,
10
- Server,
11
- Zap,
12
- MessageSquare,
13
- Search as SearchIcon,
14
- Database,
15
- Waypoints,
16
- SquareMousePointer,
17
- ListOrdered,
18
- ArrowLeftRight,
19
- Package,
20
- Box,
21
- Bot,
22
- } from 'lucide-react';
23
- import type { NavNode } from '@stores/sidebar-store/state';
24
- import { getBadgeClasses } from './utils';
25
-
26
- const cn = (...classes: (string | false | undefined)[]) => classes.filter(Boolean).join(' ');
27
-
28
- type SearchResult = {
29
- nodeKey: string;
30
- node: NavNode;
31
- matchType: 'name' | 'id';
32
- };
33
-
34
- type Props = {
35
- nodes: Record<string, NavNode | string>;
36
- onSelectResult: (nodeKey: string, node: NavNode) => void;
37
- onSearchChange?: (isSearching: boolean) => void;
38
- };
39
-
40
- export default function SearchBar({ nodes, onSelectResult, onSearchChange }: Props) {
41
- const [searchQuery, setSearchQuery] = useState('');
42
- const [searchFilters, setSearchFilters] = useState<Set<string>>(new Set());
43
- const [showFilterDropdown, setShowFilterDropdown] = useState(false);
44
-
45
- // Pre-process searchable nodes to avoid iterating object on every render
46
- // Filter out unversioned keys (e.g., "domain:OrderService") to avoid duplicates with versioned keys (e.g., "domain:OrderService:1.0.0")
47
- const searchableNodes = useMemo((): Array<[string, NavNode]> => {
48
- const result: Array<[string, NavNode]> = [];
49
- for (const [key, node] of Object.entries(nodes)) {
50
- // Skip string references (unversioned aliases that point to versioned keys)
51
- if (typeof node === 'string') continue;
52
- if (node.type === 'group') continue;
53
- // Only include versioned keys (those with 3+ parts like "type:id:version")
54
- // Unversioned keys (2 parts like "type:id") are aliases to latest version and would cause duplicates
55
- const keyParts = key.split(':');
56
- if (keyParts.length >= 3) {
57
- result.push([key, node]);
58
- }
59
- }
60
- return result;
61
- }, [nodes]);
62
-
63
- // Get available badges from nodes
64
- const availableBadges = useMemo(() => {
65
- const badges = new Set<string>();
66
-
67
- for (const [_, node] of searchableNodes) {
68
- if (node.badge) {
69
- badges.add(node.badge);
70
- }
71
- }
72
- return badges;
73
- }, [searchableNodes]);
74
-
75
- const handleSearchChange = (value: string) => {
76
- setSearchQuery(value);
77
- onSearchChange?.(value.trim().length > 0);
78
- };
79
-
80
- const clearSearch = () => {
81
- setSearchQuery('');
82
- onSearchChange?.(false);
83
- };
84
-
85
- const filterTypes = [
86
- { key: 'channel', label: 'Channels', badge: 'Channel', icon: ArrowLeftRight },
87
- { key: 'agent', label: 'Agents', badge: 'Agent', icon: Bot },
88
- { key: 'command', label: 'Commands', badge: 'Command', icon: MessageSquare },
89
- { key: 'container', label: 'Data Stores', badge: 'Container', icon: Database },
90
- { key: 'data-product', label: 'Data Products', badge: 'Data Product', icon: Package },
91
- { key: 'design', label: 'Designs', badge: 'Design', icon: SquareMousePointer },
92
- { key: 'domain', label: 'Domains', badge: 'Domain', icon: Boxes },
93
- { key: 'entity', label: 'Entities', badge: 'Entity', icon: Box },
94
- { key: 'event', label: 'Events', badge: 'Event', icon: Zap },
95
- { key: 'flow', label: 'Flows', badge: 'Flow', icon: Waypoints },
96
- { key: 'query', label: 'Queries', badge: 'Query', icon: SearchIcon },
97
- { key: 'service', label: 'Services', badge: 'Service', icon: Server },
98
- ].filter((filter) => availableBadges.has(filter.badge));
99
-
100
- const toggleSearchFilter = (filterKey: string) => {
101
- setSearchFilters((prev) => {
102
- const next = new Set(prev);
103
- if (next.has(filterKey)) {
104
- next.delete(filterKey);
105
- } else {
106
- next.add(filterKey);
107
- }
108
- return next;
109
- });
110
- };
111
-
112
- const searchResults = useCallback((): SearchResult[] => {
113
- if (!searchQuery.trim()) return [];
114
-
115
- const query = searchQuery.toLowerCase();
116
- const results: SearchResult[] = [];
117
-
118
- const badgeToFilterKey: Record<string, string> = {
119
- Agent: 'agent',
120
- Domain: 'domain',
121
- Service: 'service',
122
- Event: 'event',
123
- Command: 'command',
124
- Query: 'query',
125
- Container: 'container',
126
- 'Data Product': 'data-product',
127
- Flow: 'flow',
128
- Design: 'design',
129
- Channel: 'channel',
130
- Entity: 'entity',
131
- };
132
-
133
- // Use the memoized array instead of Object.entries(nodes)
134
- for (const [key, node] of searchableNodes) {
135
- if (searchFilters.size > 0) {
136
- const filterKey = node.badge ? badgeToFilterKey[node.badge] : null;
137
- if (!filterKey || !searchFilters.has(filterKey)) continue;
138
- }
139
-
140
- if (node.title.toLowerCase().includes(query)) {
141
- results.push({ nodeKey: key, node, matchType: 'name' });
142
- continue;
143
- }
144
-
145
- const keyParts = key.split(':');
146
- if (keyParts.length >= 3) {
147
- const id = keyParts[1].toLowerCase();
148
- if (id.includes(query)) {
149
- results.push({ nodeKey: key, node, matchType: 'id' });
150
- }
151
- }
152
- }
153
-
154
- return results
155
- .sort((a, b) => {
156
- const aExact = a.node.title.toLowerCase() === query;
157
- const bExact = b.node.title.toLowerCase() === query;
158
- if (aExact && !bExact) return -1;
159
- if (!aExact && bExact) return 1;
160
- return a.node.title.localeCompare(b.node.title);
161
- })
162
- .slice(0, 20);
163
- }, [searchQuery, searchableNodes, searchFilters]);
164
-
165
- const handleSelectResult = (nodeKey: string, node: NavNode) => {
166
- onSelectResult(nodeKey, node);
167
- clearSearch();
168
- };
169
-
170
- const results = searchResults();
171
- const showSearchResults = searchQuery.trim().length > 0;
172
-
173
- // Group results by type (badge), sorted alphabetically
174
- const groupedResults = useMemo(() => {
175
- const groups: Record<string, SearchResult[]> = {};
176
-
177
- for (const result of results) {
178
- const badge = result.node.badge || 'Other';
179
- if (!groups[badge]) {
180
- groups[badge] = [];
181
- }
182
- groups[badge].push(result);
183
- }
184
-
185
- return Object.entries(groups)
186
- .sort(([a], [b]) => a.localeCompare(b))
187
- .map(([type, typeResults]) => ({ type, results: typeResults }));
188
- }, [results]);
189
-
190
- return (
191
- <>
192
- {/* Search Input */}
193
- <div className="px-4 py-3 bg-[rgb(var(--ec-content-bg))] border-b border-[rgb(var(--ec-content-border))]">
194
- <div className="flex gap-2">
195
- <div className="relative flex-1">
196
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[rgb(var(--ec-input-placeholder))]" />
197
- <input
198
- type="text"
199
- placeholder="Search resources..."
200
- value={searchQuery}
201
- onChange={(e) => handleSearchChange(e.target.value)}
202
- className="w-full pl-9! pr-8! py-2.5! text-sm! bg-[rgb(var(--ec-input-bg))]! border! border-[rgb(var(--ec-input-border))]! rounded-xl! focus:outline-hidden! focus:ring-2! focus:ring-[rgb(var(--ec-accent))]! focus:border-transparent! text-[rgb(var(--ec-input-text))]! placeholder:text-[rgb(var(--ec-input-placeholder))]!"
203
- />
204
- {searchQuery && (
205
- <button
206
- onClick={clearSearch}
207
- className="absolute right-3 top-1/2 -translate-y-1/2 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-icon-hover))]"
208
- aria-label="Clear search"
209
- >
210
- <X className="w-4 h-4" aria-hidden="true" />
211
- </button>
212
- )}
213
- </div>
214
- {/* Filter Button */}
215
- <div className="relative">
216
- <button
217
- onClick={() => setShowFilterDropdown(!showFilterDropdown)}
218
- className={cn(
219
- 'flex items-center justify-center w-10 h-10 rounded-xl border transition-colors',
220
- searchFilters.size > 0
221
- ? 'bg-[rgb(var(--ec-accent)/0.1)] border-[rgb(var(--ec-accent)/0.3)] text-[rgb(var(--ec-accent))]'
222
- : 'bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-icon-hover))] hover:bg-[rgb(var(--ec-content-hover))]'
223
- )}
224
- aria-label="Filter search results"
225
- aria-expanded={showFilterDropdown}
226
- >
227
- <SlidersHorizontal className="w-4 h-4" aria-hidden="true" />
228
- {searchFilters.size > 0 && (
229
- <span className="absolute -top-1 -right-1 w-4 h-4 bg-[rgb(var(--ec-accent))] text-white text-[10px] font-bold rounded-full flex items-center justify-center">
230
- {searchFilters.size}
231
- </span>
232
- )}
233
- </button>
234
-
235
- {/* Filter Dropdown */}
236
- {showFilterDropdown && (
237
- <>
238
- <div className="fixed inset-0 z-20" onClick={() => setShowFilterDropdown(false)} />
239
- <div className="absolute right-0 top-full mt-1 w-48 bg-[rgb(var(--ec-dropdown-bg))] rounded-lg shadow-lg border border-[rgb(var(--ec-dropdown-border))] z-30">
240
- <div className="p-2">
241
- <div className="text-[10px] font-medium text-[rgb(var(--ec-content-text-muted))] uppercase tracking-wide px-2 py-1">
242
- Filter by type
243
- </div>
244
- <div className="flex flex-col gap-0.5 mt-1">
245
- {filterTypes.map((filter) => {
246
- const isActive = searchFilters.has(filter.key);
247
- const Icon = filter.icon;
248
- return (
249
- <button
250
- key={filter.key}
251
- onClick={() => toggleSearchFilter(filter.key)}
252
- className={cn(
253
- 'flex items-center justify-between px-2 py-1.5 rounded text-sm transition-colors',
254
- isActive
255
- ? 'bg-[rgb(var(--ec-accent)/0.1)] text-[rgb(var(--ec-accent))]'
256
- : 'text-[rgb(var(--ec-dropdown-text))] hover:bg-[rgb(var(--ec-dropdown-hover))]'
257
- )}
258
- >
259
- <span className="flex items-center gap-2">
260
- <Icon className="w-3 h-3" />
261
- {filter.label}
262
- </span>
263
- {isActive && <span className="text-[rgb(var(--ec-accent))]">✓</span>}
264
- </button>
265
- );
266
- })}
267
- </div>
268
- {searchFilters.size > 0 && (
269
- <>
270
- <div className="border-t border-[rgb(var(--ec-content-border))] my-2" />
271
- <button
272
- onClick={() => {
273
- setSearchFilters(new Set());
274
- setShowFilterDropdown(false);
275
- }}
276
- className="w-full px-2 py-1.5 text-sm text-[rgb(var(--ec-content-text-muted))] hover:text-[rgb(var(--ec-content-text))] hover:bg-[rgb(var(--ec-dropdown-hover))] rounded text-left"
277
- >
278
- Reset filters
279
- </button>
280
- </>
281
- )}
282
- </div>
283
- </div>
284
- </>
285
- )}
286
- </div>
287
- </div>
288
- </div>
289
-
290
- {/* Search Results */}
291
- {showSearchResults && (
292
- <div className="flex-1 overflow-y-auto bg-[rgb(var(--ec-content-bg))] border-b border-[rgb(var(--ec-content-border))]">
293
- <div className="px-3 py-2">
294
- <div className="text-[10px] font-medium text-[rgb(var(--ec-content-text-muted))] uppercase tracking-wide mb-2">
295
- {results.length > 0 ? `${results.length} result${results.length > 1 ? 's' : ''}` : 'No results'}
296
- </div>
297
- {groupedResults.length > 0 && (
298
- <div className="flex flex-col gap-3">
299
- {groupedResults.map(({ type, results: typeResults }) => (
300
- <div key={type}>
301
- <div className="flex items-center gap-2 mb-1">
302
- <span
303
- className={cn(
304
- 'px-1.5 py-0.5 text-[8px] font-semibold uppercase tracking-wide rounded',
305
- getBadgeClasses(type)
306
- )}
307
- >
308
- {type}
309
- </span>
310
- <span className="text-[10px] text-[rgb(var(--ec-content-text-muted))]">({typeResults.length})</span>
311
- </div>
312
- <div className="flex flex-col gap-0.5">
313
- {typeResults.map(({ nodeKey, node, matchType }) => (
314
- <button
315
- key={nodeKey}
316
- onClick={() => handleSelectResult(nodeKey, node)}
317
- className="group flex items-center justify-between w-full px-3 py-2 rounded-lg cursor-pointer text-left transition-colors hover:bg-[rgb(var(--ec-content-hover))]"
318
- >
319
- <div className="flex flex-col min-w-0 flex-1">
320
- <span className="text-sm text-[rgb(var(--ec-content-text))] truncate">{node.title}</span>
321
- {matchType === 'id' && (
322
- <span className="text-xs text-[rgb(var(--ec-content-text-muted))] truncate">
323
- ID: {nodeKey.split(':')[2]}
324
- </span>
325
- )}
326
- </div>
327
- <div className="flex items-center gap-2 flex-shrink-0">
328
- {node.pages && node.pages.length > 0 && (
329
- <ChevronRight className="w-4 h-4 text-[rgb(var(--ec-icon-color))] group-hover:text-[rgb(var(--ec-accent))]" />
330
- )}
331
- </div>
332
- </button>
333
- ))}
334
- </div>
335
- </div>
336
- ))}
337
- </div>
338
- )}
339
- {results.length === 0 && searchQuery.trim() && (
340
- <div className="text-sm text-[rgb(var(--ec-content-text-muted))] py-4 text-center">
341
- No resources found for "{searchQuery}"
342
- </div>
343
- )}
344
- </div>
345
- </div>
346
- )}
347
- </>
348
- );
349
- }