@eventcatalog/core 5.9.1 → 5.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-3PWCUWQC.js → chunk-5QOG7UYZ.js} +1 -1
- package/dist/{chunk-KGFAIGOT.js → chunk-EETSYUKN.js} +1 -1
- package/dist/{chunk-A3256YCN.js → chunk-EGOTM5ZG.js} +1 -1
- package/dist/{chunk-YBM7MCOY.js → chunk-EXBUNFIM.js} +1 -1
- package/dist/{chunk-JDSFD7IC.js → chunk-ZE2RPUNY.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/development/guides/resources/entities/05-entity-maps.md +5 -0
- package/dist/docs/development/guides/resources/entities/06-global-entity-map.md +101 -0
- package/dist/docs/development/guides/resources/entities/{06-reference.md → 07-reference.md} +2 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +8 -8
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/MDX/EntityMap/CatalogEntityMap.tsx +301 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +61 -61
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +72 -72
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +1 -1
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +2 -2
- package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +1 -1
- package/eventcatalog/src/features/studio/webmcp-relay/README.md +1 -1
- package/eventcatalog/src/features/studio/webmcp-relay/embed.js +1 -2
- package/eventcatalog/src/features/studio/webmcp-relay/widget.html +2 -1
- package/eventcatalog/src/features/studio/webmcp-relay/widget.js +2 -1
- package/eventcatalog/src/pages/visualiser/entity-map/index.astro +47 -0
- package/eventcatalog/src/pages/visualiser/graph/index.astro +2 -2
- package/eventcatalog/src/stores/sidebar-store/state.ts +10 -1
- package/eventcatalog/src/utils/node-graphs/catalog-entity-map-view.ts +82 -0
- package/eventcatalog/src/utils/node-graphs/catalog-entity-map.ts +113 -0
- package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +17 -61
- package/eventcatalog/src/utils/node-graphs/entity-layout.ts +53 -0
- package/eventcatalog/src/utils/node-graphs/flow-lanes.ts +2 -2
- package/package.json +5 -5
|
@@ -169,7 +169,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
|
|
|
169
169
|
var import_os = __toESM(require("os"), 1);
|
|
170
170
|
|
|
171
171
|
// package.json
|
|
172
|
-
var version = "5.
|
|
172
|
+
var version = "5.10.0";
|
|
173
173
|
|
|
174
174
|
// src/constants.ts
|
|
175
175
|
var VERSION = version;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-5QOG7UYZ.js";
|
|
4
4
|
import "../chunk-AD7CL772.js";
|
|
5
|
-
import "../chunk-
|
|
5
|
+
import "../chunk-EXBUNFIM.js";
|
|
6
6
|
import "../chunk-YRZK7HKX.js";
|
|
7
7
|
import "../chunk-CA6WCQ2T.js";
|
|
8
8
|
import "../chunk-CPW6G62V.js";
|
|
9
|
-
import "../chunk-
|
|
9
|
+
import "../chunk-EETSYUKN.js";
|
|
10
10
|
import "../chunk-4G7FSRPN.js";
|
|
11
11
|
export {
|
|
12
12
|
log_build_default as default
|
package/dist/constants.cjs
CHANGED
package/dist/constants.js
CHANGED
|
@@ -44,7 +44,12 @@ You can also embed an entity map on a page using the `<EntityMap />` component.
|
|
|
44
44
|
|
|
45
45
|
Use embedded entity maps when you want the model to appear directly in domain, service, or custom documentation.
|
|
46
46
|
|
|
47
|
+
## See every entity at once
|
|
48
|
+
|
|
49
|
+
Entity maps show the entities of one domain or service. To see every entity in your catalog, and how entities relate across domains, open the [Global Entity Map](/docs/development/guides/resources/entities/global-entity-map).
|
|
50
|
+
|
|
47
51
|
## Next steps
|
|
48
52
|
|
|
49
53
|
- [Model entity relationships](/docs/development/guides/resources/entities/model-entity-relationships)
|
|
54
|
+
- [Global entity map](/docs/development/guides/resources/entities/global-entity-map)
|
|
50
55
|
- [EntityMap component reference](/docs/development/components/components/entitymap)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
keywords:
|
|
3
|
+
- EventCatalog entities
|
|
4
|
+
- Global entity map
|
|
5
|
+
- Entity relationship diagram
|
|
6
|
+
- Data model
|
|
7
|
+
sidebar_position: 6
|
|
8
|
+
sidebar_label: Global entity map
|
|
9
|
+
title: Global entity map
|
|
10
|
+
description: See every entity in your catalog, and how they relate across domains, on one diagram.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
import AddedIn from '@site/src/components/MDX/AddedIn';
|
|
14
|
+
|
|
15
|
+
<AddedIn version="5.10.0" />
|
|
16
|
+
|
|
17
|
+
The Global Entity Map shows every entity in your catalog on one diagram, with the references between them. Where an [entity map](/docs/development/guides/resources/entities/entity-maps) shows the entities of one domain or service, the Global Entity Map shows the data model of the whole organization.
|
|
18
|
+
|
|
19
|
+
Use it to answer questions like:
|
|
20
|
+
|
|
21
|
+
- Which entities does each domain own?
|
|
22
|
+
- Which entities are referenced from other domains?
|
|
23
|
+
- Which entities are shared by more than one domain?
|
|
24
|
+
- Which entities don't belong to anything yet?
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
## Open the Global Entity Map
|
|
29
|
+
|
|
30
|
+
When your catalog has entities, a **Global Entity Map** link appears in the **Top level diagrams** section of the visualizer sidebar. You can also open it at:
|
|
31
|
+
|
|
32
|
+
```txt
|
|
33
|
+
/visualiser/entity-map
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The map shows the latest version of every entity.
|
|
37
|
+
|
|
38
|
+
If you've [customized your sidebar](/docs/development/customization/documentation-sidebar#custom-groups-and-links), you can add a custom link to this URL.
|
|
39
|
+
|
|
40
|
+
## How entities are placed in domains
|
|
41
|
+
|
|
42
|
+
[Entities can be added to domains, systems, and services](/docs/development/guides/resources/entities/add-entities-to-resources). The Global Entity Map works out which domain each entity belongs to from any of them:
|
|
43
|
+
|
|
44
|
+
| Entity listed by | Belongs to |
|
|
45
|
+
|------------------|------------|
|
|
46
|
+
| A domain | That domain. |
|
|
47
|
+
| A system | The system, and the domain the system is in. |
|
|
48
|
+
| A service | The service's system (if it's in one), and its domain. |
|
|
49
|
+
| More than one domain | **Shared across domains**, a group of its own. |
|
|
50
|
+
| Nothing | No domain. It's shown outside every group. |
|
|
51
|
+
|
|
52
|
+
A resource that pins an older version of an entity doesn't own the latest version. For example, if a domain lists `customer` at version `1.0.0` and the latest is `2.0.0`, that domain doesn't own the `customer` shown on the map.
|
|
53
|
+
|
|
54
|
+
An entity's team is its own owner, or else the owner of the domain, system, or service that lists it.
|
|
55
|
+
|
|
56
|
+
## Group entities
|
|
57
|
+
|
|
58
|
+
The map opens with entities grouped by their domain. Use **Group by** at the top right of the diagram (or press <kbd>G</kbd>) to group them another way:
|
|
59
|
+
|
|
60
|
+
| Group by | What it shows |
|
|
61
|
+
|----------|---------------|
|
|
62
|
+
| **Domain** | A box for each domain, and one for entities shared across domains. |
|
|
63
|
+
| **System** | A box for each system. Only entities listed by a system, or by a service in one, are grouped. |
|
|
64
|
+
| **Team** | A box for each owning team. |
|
|
65
|
+
| **Domain, then system** | Systems nested in their domain. |
|
|
66
|
+
| **Nothing** | No groups. |
|
|
67
|
+
|
|
68
|
+
Groups can be shown as **Boxes** placed around their entities, or as **Lanes** stacked from top to bottom.
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
## Filter what's shown
|
|
73
|
+
|
|
74
|
+
The panel on the left of the diagram lists each domain, with how many entities it has and how many references cross into other domains.
|
|
75
|
+
|
|
76
|
+
- Click a domain to show or hide its entities.
|
|
77
|
+
- Hover over a domain and click the button that appears beside it (**Only show**) to show just that domain. Click it again to show everything.
|
|
78
|
+
- Use **Show all** and **Hide all** to change every domain at once, and **Filter domains** to find a domain in a long list.
|
|
79
|
+
- Turn on **Only references across domains** to show only the entities that reference, or are referenced by, an entity in another domain. This is a quick way to see the dependencies between domains.
|
|
80
|
+
|
|
81
|
+

|
|
82
|
+
|
|
83
|
+
To give the diagram more room, collapse the panel with the button at its top right (or press <kbd>[</kbd>). The collapsed panel shows how many filters are on, so hidden domains aren't forgotten. The map remembers whether the panel is open in your browser.
|
|
84
|
+
|
|
85
|
+
## Share a view
|
|
86
|
+
|
|
87
|
+
The domains you've hidden, the **Only references across domains** filter, and the grouping are all kept in the URL. Copy the URL to share exactly what you're looking at. For example:
|
|
88
|
+
|
|
89
|
+
```txt
|
|
90
|
+
/visualiser/entity-map?hide=reviews,shopping&crossDomain=true&group=team
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Find an entity
|
|
94
|
+
|
|
95
|
+
Use **Search nodes** at the top of the diagram to find an entity. Picking one moves the diagram to it and selects it.
|
|
96
|
+
|
|
97
|
+
## Next steps
|
|
98
|
+
|
|
99
|
+
- [Add entities to resources](/docs/development/guides/resources/entities/add-entities-to-resources)
|
|
100
|
+
- [Model entity relationships](/docs/development/guides/resources/entities/model-entity-relationships)
|
|
101
|
+
- [Entity maps](/docs/development/guides/resources/entities/entity-maps)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
keywords:
|
|
3
3
|
- EventCatalog entities
|
|
4
4
|
- Entity frontmatter
|
|
5
|
-
sidebar_position:
|
|
5
|
+
sidebar_position: 7
|
|
6
6
|
sidebar_label: Reference
|
|
7
7
|
title: Entities reference
|
|
8
8
|
description: Frontmatter fields, paths, and routes for entities in EventCatalog.
|
|
@@ -43,6 +43,7 @@ Nested entities use the same `versioned/{version}/index.mdx` pattern inside thei
|
|
|
43
43
|
| `/visualiser/entities/{entity-id}/{version}` | Entity resource diagram. |
|
|
44
44
|
| `/visualiser/domains/{domain-id}/{version}/entity-map` | Entity Diagram for entities assigned to a domain. |
|
|
45
45
|
| `/visualiser/services/{service-id}/{version}/entity-map` | Entity Diagram for entities assigned to a service. |
|
|
46
|
+
| `/visualiser/entity-map` | [Global Entity Map](/docs/development/guides/resources/entities/global-entity-map): every entity in the catalog, grouped by domain. |
|
|
46
47
|
|
|
47
48
|
## Frontmatter example
|
|
48
49
|
|
package/dist/eventcatalog.cjs
CHANGED
|
@@ -175,7 +175,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
|
|
|
175
175
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
176
176
|
|
|
177
177
|
// package.json
|
|
178
|
-
var version = "5.
|
|
178
|
+
var version = "5.10.0";
|
|
179
179
|
|
|
180
180
|
// src/constants.ts
|
|
181
181
|
var VERSION = version;
|
package/dist/eventcatalog.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
stopChildWithParent
|
|
3
|
+
} from "./chunk-S5GWAUG5.js";
|
|
1
4
|
import {
|
|
2
5
|
runMigrations
|
|
3
6
|
} from "./chunk-XUAF2H54.js";
|
|
4
7
|
import "./chunk-CA4U2JP7.js";
|
|
5
|
-
import {
|
|
6
|
-
stopChildWithParent
|
|
7
|
-
} from "./chunk-S5GWAUG5.js";
|
|
8
8
|
import {
|
|
9
9
|
log_build_default
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-5QOG7UYZ.js";
|
|
11
11
|
import {
|
|
12
12
|
getLicenseStatus,
|
|
13
13
|
printLicenseStatus
|
|
14
14
|
} from "./chunk-AD7CL772.js";
|
|
15
|
-
import "./chunk-
|
|
15
|
+
import "./chunk-EXBUNFIM.js";
|
|
16
16
|
import "./chunk-YRZK7HKX.js";
|
|
17
17
|
import "./chunk-CA6WCQ2T.js";
|
|
18
18
|
import "./chunk-CPW6G62V.js";
|
|
@@ -39,10 +39,10 @@ import {
|
|
|
39
39
|
} from "./chunk-NKNOXQ62.js";
|
|
40
40
|
import {
|
|
41
41
|
generate
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-EGOTM5ZG.js";
|
|
43
43
|
import {
|
|
44
44
|
logger
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-ZE2RPUNY.js";
|
|
46
46
|
import {
|
|
47
47
|
resolve_catalog_dependencies_default
|
|
48
48
|
} from "./chunk-PD7KTSYN.js";
|
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
} from "./chunk-LFQJXLHG.js";
|
|
80
80
|
import {
|
|
81
81
|
VERSION
|
|
82
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-EETSYUKN.js";
|
|
83
83
|
import {
|
|
84
84
|
getEventCatalogConfigFile,
|
|
85
85
|
verifyRequiredFieldsAreInCatalogConfigFile
|
package/dist/generate.cjs
CHANGED
|
@@ -106,7 +106,7 @@ var getEventCatalogConfigFile = async (projectDirectory) => {
|
|
|
106
106
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
107
107
|
|
|
108
108
|
// package.json
|
|
109
|
-
var version = "5.
|
|
109
|
+
var version = "5.10.0";
|
|
110
110
|
|
|
111
111
|
// src/constants.ts
|
|
112
112
|
var VERSION = version;
|
package/dist/generate.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generate
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-EGOTM5ZG.js";
|
|
4
|
+
import "./chunk-ZE2RPUNY.js";
|
|
5
|
+
import "./chunk-EETSYUKN.js";
|
|
6
6
|
import "./chunk-4G7FSRPN.js";
|
|
7
7
|
export {
|
|
8
8
|
generate
|
package/dist/utils/cli-logger.js
CHANGED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entity map of the whole catalog: every entity and the references between them, beside a list of
|
|
3
|
+
* the domains they belong to, to show or hide each one. Only references that cross domains can be
|
|
4
|
+
* shown too. What's hidden is kept in the URL (`?hide=`), so a view can be shared.
|
|
5
|
+
*
|
|
6
|
+
* Grouping (by domain, system or team) is the visualiser's own "Group by" picker, kept in the URL too,
|
|
7
|
+
* so it carries over when the graph is drawn again with other domains hidden, a reload or a shared link
|
|
8
|
+
* (grouping by nothing included). The page starts grouped by domain only when the URL says nothing.
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useMemo, useState, type ComponentProps } from 'react';
|
|
11
|
+
import { createPortal } from 'react-dom';
|
|
12
|
+
import type { Edge, Node } from '@xyflow/react';
|
|
13
|
+
import { Boxes, Eye, EyeOff, PanelLeftClose, PanelLeftOpen, Search, Shuffle } from 'lucide-react';
|
|
14
|
+
import AstroNodeGraph from '@components/MDX/NodeGraph/AstroNodeGraph';
|
|
15
|
+
import { getCrossDomainEdges, getDomainSummaries, getShownGraph } from '@utils/node-graphs/catalog-entity-map-view';
|
|
16
|
+
|
|
17
|
+
type Props = ComponentProps<typeof AstroNodeGraph> & { sharedLaneId: string };
|
|
18
|
+
|
|
19
|
+
const HIDE_PARAM = 'hide';
|
|
20
|
+
const CROSS_DOMAIN_PARAM = 'crossDomain';
|
|
21
|
+
// Whether the filters are collapsed, remembered in this browser (storage can be unavailable)
|
|
22
|
+
const COLLAPSED_KEY = 'eventcatalog:entity-map:filters-collapsed';
|
|
23
|
+
const PANEL_WIDTH = 288;
|
|
24
|
+
const RAIL_WIDTH = 44;
|
|
25
|
+
|
|
26
|
+
const readCollapsed = () => {
|
|
27
|
+
try {
|
|
28
|
+
return window.localStorage.getItem(COLLAPSED_KEY) === 'true';
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const writeCollapsed = (collapsed: boolean) => {
|
|
34
|
+
try {
|
|
35
|
+
window.localStorage.setItem(COLLAPSED_KEY, String(collapsed));
|
|
36
|
+
} catch {
|
|
37
|
+
// Not remembered, then
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const isTyping = (target: EventTarget | null) =>
|
|
41
|
+
target instanceof HTMLElement && (target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName));
|
|
42
|
+
|
|
43
|
+
const readUrl = () => {
|
|
44
|
+
const params = new URLSearchParams(window.location.search);
|
|
45
|
+
return {
|
|
46
|
+
hidden: new Set((params.get(HIDE_PARAM) || '').split(',').filter(Boolean)),
|
|
47
|
+
crossDomainOnly: params.get(CROSS_DOMAIN_PARAM) === 'true',
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const writeUrl = (hidden: Set<string>, crossDomainOnly: boolean) => {
|
|
52
|
+
const url = new URL(window.location.href);
|
|
53
|
+
if (hidden.size > 0) url.searchParams.set(HIDE_PARAM, [...hidden].join(','));
|
|
54
|
+
else url.searchParams.delete(HIDE_PARAM);
|
|
55
|
+
if (crossDomainOnly) url.searchParams.set(CROSS_DOMAIN_PARAM, 'true');
|
|
56
|
+
else url.searchParams.delete(CROSS_DOMAIN_PARAM);
|
|
57
|
+
window.history.replaceState(window.history.state, '', url);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default function CatalogEntityMap({ nodes, edges, sharedLaneId, ...graphProps }: Props) {
|
|
61
|
+
const [{ hidden, crossDomainOnly }, setView] = useState(readUrl);
|
|
62
|
+
const [query, setQuery] = useState('');
|
|
63
|
+
const [collapsed, setCollapsed] = useState(readCollapsed);
|
|
64
|
+
// The graph laid out again for a filtered view, with the view it's for (`key`), so a layout is never
|
|
65
|
+
// shown for another view while that one's is being made
|
|
66
|
+
const [laidOut, setLaidOut] = useState<{ key: string; nodes: Node[]; edges: Edge[] } | null>(null);
|
|
67
|
+
|
|
68
|
+
const crossDomainEdges = useMemo(() => getCrossDomainEdges(nodes, edges, sharedLaneId), [nodes, edges, sharedLaneId]);
|
|
69
|
+
const domains = useMemo(
|
|
70
|
+
() => getDomainSummaries(nodes, crossDomainEdges, sharedLaneId),
|
|
71
|
+
[nodes, crossDomainEdges, sharedLaneId]
|
|
72
|
+
);
|
|
73
|
+
const shown = useMemo(
|
|
74
|
+
() => getShownGraph({ nodes, edges, crossDomainEdges, hidden, crossDomainOnly }),
|
|
75
|
+
[nodes, edges, crossDomainEdges, hidden, crossDomainOnly]
|
|
76
|
+
);
|
|
77
|
+
// A new graph each time what's shown changes: the visualiser takes its nodes when it mounts
|
|
78
|
+
const graphKey = [...hidden].sort().join(',') + (crossDomainOnly ? '|cross' : '');
|
|
79
|
+
|
|
80
|
+
// Entities hidden leave gaps in the layout the catalog gave the whole map, so what's shown is laid out
|
|
81
|
+
// again (when grouped, the visualiser lays the groups out itself anyway)
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
writeUrl(hidden, crossDomainOnly);
|
|
84
|
+
if (shown.isEverything) return;
|
|
85
|
+
let cancelled = false;
|
|
86
|
+
// The same force layout the catalog gave the whole map. Should it fail, the entities keep their
|
|
87
|
+
// places in the whole map's layout rather than nothing being drawn
|
|
88
|
+
import('@utils/node-graphs/entity-layout')
|
|
89
|
+
.then(({ layoutEntityNodes }) => layoutEntityNodes(shown.nodes, shown.edges))
|
|
90
|
+
.catch(() => shown.nodes)
|
|
91
|
+
.then((laidOutNodes) => {
|
|
92
|
+
if (!cancelled) setLaidOut({ key: graphKey, nodes: laidOutNodes, edges: shown.edges });
|
|
93
|
+
});
|
|
94
|
+
return () => {
|
|
95
|
+
cancelled = true;
|
|
96
|
+
};
|
|
97
|
+
}, [shown, graphKey, hidden, crossDomainOnly]);
|
|
98
|
+
|
|
99
|
+
const toggle = (id: string) =>
|
|
100
|
+
setView((view) => {
|
|
101
|
+
const next = new Set(view.hidden);
|
|
102
|
+
if (next.has(id)) next.delete(id);
|
|
103
|
+
else next.add(id);
|
|
104
|
+
return { ...view, hidden: next };
|
|
105
|
+
});
|
|
106
|
+
// Shows only this domain (or everything, when it's the only one shown)
|
|
107
|
+
const solo = (id: string) =>
|
|
108
|
+
setView((view) => {
|
|
109
|
+
const others = domains.filter((domain) => domain.lane.id !== id).map((domain) => domain.lane.id);
|
|
110
|
+
const isSolo = !view.hidden.has(id) && others.every((other) => view.hidden.has(other));
|
|
111
|
+
return { ...view, hidden: isSolo ? new Set() : new Set(others) };
|
|
112
|
+
});
|
|
113
|
+
const setAll = (visible: boolean) =>
|
|
114
|
+
setView((view) => ({ ...view, hidden: visible ? new Set() : new Set(domains.map((domain) => domain.lane.id)) }));
|
|
115
|
+
|
|
116
|
+
const filteredDomains = domains.filter((domain) => domain.lane.name.toLowerCase().includes(query.toLowerCase()));
|
|
117
|
+
const graph = shown.isEverything ? { nodes, edges } : laidOut?.key === graphKey ? laidOut : null;
|
|
118
|
+
// Shown on the collapsed rail, so filters left on aren't forgotten
|
|
119
|
+
const activeFilters = hidden.size + (crossDomainOnly ? 1 : 0);
|
|
120
|
+
|
|
121
|
+
useEffect(() => writeCollapsed(collapsed), [collapsed]);
|
|
122
|
+
// [ collapses and opens the filters
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
125
|
+
if (event.key !== '[' || event.metaKey || event.ctrlKey || event.altKey || isTyping(event.target)) return;
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
setCollapsed((isCollapsed) => !isCollapsed);
|
|
128
|
+
};
|
|
129
|
+
window.addEventListener('keydown', onKeyDown);
|
|
130
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<>
|
|
135
|
+
<aside
|
|
136
|
+
style={{ width: collapsed ? RAIL_WIDTH : PANEL_WIDTH }}
|
|
137
|
+
className="relative shrink-0 border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] rounded-md overflow-hidden transition-[width] duration-300 ease-in-out motion-reduce:transition-none"
|
|
138
|
+
>
|
|
139
|
+
{/* Collapsed: a rail to open the filters again */}
|
|
140
|
+
<div
|
|
141
|
+
aria-hidden={!collapsed}
|
|
142
|
+
className={`absolute inset-0 flex flex-col items-center gap-3 py-3 transition-[opacity,visibility] duration-200 ${
|
|
143
|
+
collapsed ? 'opacity-100 visible delay-150' : 'opacity-0 invisible'
|
|
144
|
+
}`}
|
|
145
|
+
>
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
onClick={() => setCollapsed(false)}
|
|
149
|
+
title="Show filters ([)"
|
|
150
|
+
aria-label="Show filters"
|
|
151
|
+
className="p-1.5 rounded-md text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-icon-hover))] hover:bg-[rgb(var(--ec-accent-subtle)/0.5)]"
|
|
152
|
+
>
|
|
153
|
+
<PanelLeftOpen className="h-4 w-4" />
|
|
154
|
+
</button>
|
|
155
|
+
{activeFilters > 0 && (
|
|
156
|
+
<span
|
|
157
|
+
title={`${activeFilters} filter${activeFilters === 1 ? '' : 's'} on`}
|
|
158
|
+
className="min-w-[1.25rem] h-5 px-1 flex items-center justify-center rounded-full text-[10px] font-semibold bg-[rgb(var(--ec-accent))] text-[rgb(var(--ec-accent-text))]"
|
|
159
|
+
>
|
|
160
|
+
{activeFilters}
|
|
161
|
+
</span>
|
|
162
|
+
)}
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
onClick={() => setCollapsed(false)}
|
|
166
|
+
className="[writing-mode:vertical-rl] rotate-180 text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] whitespace-nowrap"
|
|
167
|
+
>
|
|
168
|
+
{shown.nodes.length} of {nodes.length} entities
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{/* Open: the filters, at their full width so nothing reflows while the panel slides */}
|
|
173
|
+
<div
|
|
174
|
+
aria-hidden={collapsed}
|
|
175
|
+
style={{ width: PANEL_WIDTH - 2 }}
|
|
176
|
+
className={`h-full flex flex-col transition-[opacity,visibility] duration-200 ${
|
|
177
|
+
collapsed ? 'opacity-0 invisible' : 'opacity-100 visible delay-100'
|
|
178
|
+
}`}
|
|
179
|
+
>
|
|
180
|
+
<div className="px-4 pt-4 pb-3 border-b border-[rgb(var(--ec-page-border))]">
|
|
181
|
+
<div className="flex items-start justify-between gap-2">
|
|
182
|
+
<h1 className="text-sm font-semibold text-[rgb(var(--ec-page-text))]">Global Entity Map</h1>
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={() => setCollapsed(true)}
|
|
186
|
+
title="Hide filters ([)"
|
|
187
|
+
aria-label="Hide filters"
|
|
188
|
+
className="-mt-1 -mr-2 p-1.5 rounded-md text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-icon-hover))] hover:bg-[rgb(var(--ec-accent-subtle)/0.5)]"
|
|
189
|
+
>
|
|
190
|
+
<PanelLeftClose className="h-4 w-4" />
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
<p className="mt-1 text-xs text-[rgb(var(--ec-page-text-muted))]">
|
|
194
|
+
{shown.nodes.length} of {nodes.length} entities · {crossDomainEdges.length} references across domains
|
|
195
|
+
</p>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<label className="flex items-center gap-2.5 px-4 py-3 border-b border-[rgb(var(--ec-page-border))] cursor-pointer text-xs text-[rgb(var(--ec-page-text))]">
|
|
199
|
+
<input
|
|
200
|
+
type="checkbox"
|
|
201
|
+
checked={crossDomainOnly}
|
|
202
|
+
onChange={(event) => setView((view) => ({ ...view, crossDomainOnly: event.target.checked }))}
|
|
203
|
+
className="accent-[rgb(var(--ec-accent))]"
|
|
204
|
+
/>
|
|
205
|
+
<Shuffle className="h-3.5 w-3.5 text-[rgb(var(--ec-icon-color))]" />
|
|
206
|
+
<span className="flex-1">Only references across domains</span>
|
|
207
|
+
</label>
|
|
208
|
+
|
|
209
|
+
<div className="px-4 pt-3 pb-2 flex items-center justify-between">
|
|
210
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
|
|
211
|
+
Domains
|
|
212
|
+
</span>
|
|
213
|
+
<span className="flex gap-2 text-[11px]">
|
|
214
|
+
<button type="button" onClick={() => setAll(true)} className="text-[rgb(var(--ec-accent))] hover:underline">
|
|
215
|
+
Show all
|
|
216
|
+
</button>
|
|
217
|
+
<button type="button" onClick={() => setAll(false)} className="text-[rgb(var(--ec-accent))] hover:underline">
|
|
218
|
+
Hide all
|
|
219
|
+
</button>
|
|
220
|
+
</span>
|
|
221
|
+
</div>
|
|
222
|
+
<div className="px-4 pb-2">
|
|
223
|
+
<div className="flex items-center gap-2 px-2 h-8 rounded-md border border-[rgb(var(--ec-input-border))] bg-[rgb(var(--ec-input-bg))]">
|
|
224
|
+
<Search className="h-3.5 w-3.5 text-[rgb(var(--ec-icon-color))]" />
|
|
225
|
+
<input
|
|
226
|
+
value={query}
|
|
227
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
228
|
+
placeholder="Filter domains"
|
|
229
|
+
className="flex-1 min-w-0 bg-transparent text-xs outline-none text-[rgb(var(--ec-input-text))]"
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<ul className="flex-1 overflow-y-auto px-2 pb-3">
|
|
235
|
+
{filteredDomains.map(({ lane, count, crossDomain }) => {
|
|
236
|
+
const visible = !hidden.has(lane.id);
|
|
237
|
+
return (
|
|
238
|
+
<li
|
|
239
|
+
key={lane.id}
|
|
240
|
+
className="group flex items-center gap-1 rounded-md hover:bg-[rgb(var(--ec-accent-subtle)/0.5)]"
|
|
241
|
+
>
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
onClick={() => toggle(lane.id)}
|
|
245
|
+
aria-pressed={visible}
|
|
246
|
+
className={`flex flex-1 min-w-0 items-center gap-2.5 px-2 py-1.5 text-left text-xs ${
|
|
247
|
+
visible ? 'text-[rgb(var(--ec-page-text))]' : 'text-[rgb(var(--ec-page-text-muted))] opacity-60'
|
|
248
|
+
}`}
|
|
249
|
+
>
|
|
250
|
+
{visible ? (
|
|
251
|
+
<Eye className="h-3.5 w-3.5 shrink-0 text-[rgb(var(--ec-icon-color))]" />
|
|
252
|
+
) : (
|
|
253
|
+
<EyeOff className="h-3.5 w-3.5 shrink-0" />
|
|
254
|
+
)}
|
|
255
|
+
<span className={`flex-1 truncate ${lane.id.startsWith('__') ? 'italic' : 'font-medium'}`}>{lane.name}</span>
|
|
256
|
+
{crossDomain > 0 && (
|
|
257
|
+
<span
|
|
258
|
+
title={`${crossDomain} references across domains`}
|
|
259
|
+
className="flex items-center gap-0.5 text-[10px] text-[rgb(var(--ec-page-text-muted))]"
|
|
260
|
+
>
|
|
261
|
+
<Shuffle className="h-3 w-3" />
|
|
262
|
+
{crossDomain}
|
|
263
|
+
</span>
|
|
264
|
+
)}
|
|
265
|
+
<span className="tabular-nums text-[11px] text-[rgb(var(--ec-page-text-muted))]">{count}</span>
|
|
266
|
+
</button>
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
onClick={() => solo(lane.id)}
|
|
270
|
+
title={`Only show ${lane.name}`}
|
|
271
|
+
className="mr-1 p-1 rounded opacity-0 group-hover:opacity-100 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-icon-hover))]"
|
|
272
|
+
>
|
|
273
|
+
<Boxes className="h-3.5 w-3.5" />
|
|
274
|
+
</button>
|
|
275
|
+
</li>
|
|
276
|
+
);
|
|
277
|
+
})}
|
|
278
|
+
</ul>
|
|
279
|
+
</div>
|
|
280
|
+
</aside>
|
|
281
|
+
|
|
282
|
+
{graph && graph.nodes.length > 0 && (
|
|
283
|
+
<AstroNodeGraph key={graphKey} {...graphProps} nodes={graph.nodes} edges={graph.edges} />
|
|
284
|
+
)}
|
|
285
|
+
{graph && graph.nodes.length === 0 && <EmptyState portalId={graphProps.portalId || `${graphProps.id}-portal`} />}
|
|
286
|
+
</>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const EmptyState = ({ portalId }: { portalId: string }) => {
|
|
291
|
+
const [container, setContainer] = useState<HTMLElement | null>(null);
|
|
292
|
+
useEffect(() => setContainer(document.getElementById(portalId)), [portalId]);
|
|
293
|
+
if (!container) return null;
|
|
294
|
+
// Drawn in the graph's box, where the graph would be
|
|
295
|
+
return createPortal(
|
|
296
|
+
<div className="flex h-full items-center justify-center text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
297
|
+
No entities to show. Pick a domain on the left.
|
|
298
|
+
</div>,
|
|
299
|
+
container
|
|
300
|
+
);
|
|
301
|
+
};
|