@dxos/plugin-explorer 0.8.4-main.3c1ae3b → 0.8.4-main.3fbcb4aa9b

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.
Files changed (186) hide show
  1. package/dist/lib/neutral/ExplorerContainer-5TOK2ZEY.mjs +40 -0
  2. package/dist/lib/neutral/ExplorerContainer-5TOK2ZEY.mjs.map +7 -0
  3. package/dist/lib/neutral/ExplorerPlugin.mjs +26 -0
  4. package/dist/lib/neutral/ExplorerPlugin.mjs.map +7 -0
  5. package/dist/lib/neutral/capabilities/index.mjs +11 -0
  6. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  7. package/dist/lib/neutral/chunk-5X5ATGCS.mjs +73 -0
  8. package/dist/lib/neutral/chunk-5X5ATGCS.mjs.map +7 -0
  9. package/dist/lib/{browser/chunk-UBHZGWZQ.mjs → neutral/chunk-HPIS2WXY.mjs} +2 -2
  10. package/dist/lib/neutral/chunk-HPIS2WXY.mjs.map +7 -0
  11. package/dist/lib/{browser/chunk-ARBGXQFH.mjs → neutral/components/index.mjs} +341 -156
  12. package/dist/lib/{browser/chunk-ARBGXQFH.mjs.map → neutral/components/index.mjs.map} +4 -4
  13. package/dist/lib/neutral/containers/index.mjs +9 -0
  14. package/dist/lib/neutral/containers/index.mjs.map +7 -0
  15. package/dist/lib/neutral/create-object-F6TKVAGV.mjs +39 -0
  16. package/dist/lib/neutral/create-object-F6TKVAGV.mjs.map +7 -0
  17. package/dist/lib/neutral/hooks/index.mjs +40 -0
  18. package/dist/lib/neutral/hooks/index.mjs.map +7 -0
  19. package/dist/lib/neutral/index.mjs +14 -0
  20. package/dist/lib/neutral/meta.json +1 -0
  21. package/dist/lib/{browser → neutral}/meta.mjs +1 -1
  22. package/dist/lib/neutral/plugin.mjs +12 -0
  23. package/dist/lib/neutral/plugin.mjs.map +7 -0
  24. package/dist/lib/neutral/react-surface-U3JEY7V7.mjs +26 -0
  25. package/dist/lib/neutral/react-surface-U3JEY7V7.mjs.map +7 -0
  26. package/dist/lib/neutral/translations.mjs +33 -0
  27. package/dist/lib/neutral/translations.mjs.map +7 -0
  28. package/dist/lib/{browser → neutral}/types/index.mjs +1 -2
  29. package/dist/types/data/cities.d.ts +4 -4
  30. package/dist/types/data/cities.d.ts.map +1 -1
  31. package/dist/types/data/countries-110m.d.ts +19 -22
  32. package/dist/types/data/countries-110m.d.ts.map +1 -1
  33. package/dist/types/src/ExplorerPlugin.d.ts +3 -1
  34. package/dist/types/src/ExplorerPlugin.d.ts.map +1 -1
  35. package/dist/types/src/ExplorerPlugin.test.d.ts +2 -0
  36. package/dist/types/src/ExplorerPlugin.test.d.ts.map +1 -0
  37. package/dist/types/src/capabilities/create-object.d.ts +11 -0
  38. package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
  39. package/dist/types/src/capabilities/index.d.ts +8 -2
  40. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  41. package/dist/types/src/capabilities/react-surface.d.ts +3 -2
  42. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  43. package/dist/types/src/components/Chart/Chart.d.ts.map +1 -1
  44. package/dist/types/src/components/Chart/Chart.stories.d.ts +4 -1
  45. package/dist/types/src/components/Chart/Chart.stories.d.ts.map +1 -1
  46. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  47. package/dist/types/src/components/Globe/Globe.stories.d.ts +5 -2
  48. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  49. package/dist/types/src/components/Graph/CanvasForceGraph.d.ts +13 -0
  50. package/dist/types/src/components/Graph/CanvasForceGraph.d.ts.map +1 -0
  51. package/dist/types/src/components/Graph/CanvasForceGraph.stories.d.ts +17 -0
  52. package/dist/types/src/components/Graph/CanvasForceGraph.stories.d.ts.map +1 -0
  53. package/dist/types/src/components/Graph/ForceGraph.d.ts +12 -5
  54. package/dist/types/src/components/Graph/ForceGraph.d.ts.map +1 -1
  55. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts +4 -2
  56. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts.map +1 -1
  57. package/dist/types/src/components/Graph/{adapter.d.ts → graph-adapter.d.ts} +2 -2
  58. package/dist/types/src/components/Graph/graph-adapter.d.ts.map +1 -0
  59. package/dist/types/src/components/Graph/index.d.ts +1 -1
  60. package/dist/types/src/components/Graph/index.d.ts.map +1 -1
  61. package/dist/types/src/components/Graph/testing.d.ts.map +1 -1
  62. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  63. package/dist/types/src/components/Tree/Tree.stories.d.ts +5 -1
  64. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  65. package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts.map +1 -1
  66. package/dist/types/src/components/Tree/layout/RadialTree.d.ts.map +1 -1
  67. package/dist/types/src/components/Tree/layout/TidyTree.d.ts.map +1 -1
  68. package/dist/types/src/components/Tree/testing/generator.d.ts.map +1 -1
  69. package/dist/types/src/components/Tree/types/tree.d.ts +18 -16
  70. package/dist/types/src/components/Tree/types/tree.d.ts.map +1 -1
  71. package/dist/types/src/components/Tree/types/types.d.ts +1 -1
  72. package/dist/types/src/components/Tree/types/types.d.ts.map +1 -1
  73. package/dist/types/src/components/index.d.ts +0 -2
  74. package/dist/types/src/components/index.d.ts.map +1 -1
  75. package/dist/types/src/components/plot.d.ts.map +1 -1
  76. package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts +6 -0
  77. package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts.map +1 -0
  78. package/dist/types/src/containers/ExplorerContainer/index.d.ts +2 -0
  79. package/dist/types/src/containers/ExplorerContainer/index.d.ts.map +1 -0
  80. package/dist/types/src/containers/index.d.ts +3 -0
  81. package/dist/types/src/containers/index.d.ts.map +1 -0
  82. package/dist/types/src/hooks/useGraphModel.d.ts.map +1 -1
  83. package/dist/types/src/index.d.ts +1 -3
  84. package/dist/types/src/index.d.ts.map +1 -1
  85. package/dist/types/src/meta.d.ts +2 -2
  86. package/dist/types/src/meta.d.ts.map +1 -1
  87. package/dist/types/src/plugin.d.ts +3 -0
  88. package/dist/types/src/plugin.d.ts.map +1 -0
  89. package/dist/types/src/translations.d.ts +29 -27
  90. package/dist/types/src/translations.d.ts.map +1 -1
  91. package/dist/types/src/types/ExplorerAction.d.ts +0 -17
  92. package/dist/types/src/types/ExplorerAction.d.ts.map +1 -1
  93. package/dist/types/src/types/Graph.d.ts +10 -19
  94. package/dist/types/src/types/Graph.d.ts.map +1 -1
  95. package/dist/types/tsconfig.tsbuildinfo +1 -1
  96. package/package.json +107 -61
  97. package/src/ExplorerPlugin.test.ts +26 -0
  98. package/src/ExplorerPlugin.tsx +15 -45
  99. package/src/capabilities/create-object.ts +36 -0
  100. package/src/capabilities/index.ts +3 -3
  101. package/src/capabilities/react-surface.tsx +24 -18
  102. package/src/components/Chart/Chart.stories.tsx +16 -23
  103. package/src/components/Globe/Globe.stories.tsx +19 -22
  104. package/src/components/Graph/CanvasForceGraph.stories.tsx +83 -0
  105. package/src/components/Graph/CanvasForceGraph.tsx +124 -0
  106. package/src/components/Graph/ForceGraph.stories.tsx +78 -43
  107. package/src/components/Graph/ForceGraph.tsx +104 -85
  108. package/src/components/Graph/{adapter.ts → graph-adapter.ts} +14 -8
  109. package/src/components/Graph/index.ts +1 -1
  110. package/src/components/Graph/testing.ts +3 -3
  111. package/src/components/Tree/Tree.stories.tsx +44 -36
  112. package/src/components/Tree/Tree.tsx +8 -3
  113. package/src/components/Tree/testing/generator.ts +4 -2
  114. package/src/components/Tree/types/tree.test.ts +5 -4
  115. package/src/components/Tree/types/tree.ts +41 -20
  116. package/src/components/Tree/types/types.ts +1 -1
  117. package/src/components/index.ts +0 -4
  118. package/src/containers/ExplorerContainer/ExplorerContainer.tsx +51 -0
  119. package/src/containers/ExplorerContainer/index.ts +5 -0
  120. package/src/containers/index.ts +7 -0
  121. package/src/hooks/useGraphModel.ts +17 -10
  122. package/src/index.ts +1 -4
  123. package/src/meta.ts +3 -3
  124. package/src/plugin.ts +9 -0
  125. package/src/translations.ts +14 -13
  126. package/src/types/ExplorerAction.ts +1 -18
  127. package/src/types/Graph.ts +14 -28
  128. package/src/typings.d.ts +8 -0
  129. package/dist/lib/browser/ExplorerContainer-NOLLVUTE.mjs +0 -50
  130. package/dist/lib/browser/ExplorerContainer-NOLLVUTE.mjs.map +0 -7
  131. package/dist/lib/browser/chunk-2MKBRIUT.mjs +0 -31
  132. package/dist/lib/browser/chunk-2MKBRIUT.mjs.map +0 -7
  133. package/dist/lib/browser/chunk-6BVXZQPP.mjs +0 -188
  134. package/dist/lib/browser/chunk-6BVXZQPP.mjs.map +0 -7
  135. package/dist/lib/browser/chunk-P6FFFVPM.mjs +0 -100
  136. package/dist/lib/browser/chunk-P6FFFVPM.mjs.map +0 -7
  137. package/dist/lib/browser/chunk-UBHZGWZQ.mjs.map +0 -7
  138. package/dist/lib/browser/index.mjs +0 -112
  139. package/dist/lib/browser/index.mjs.map +0 -7
  140. package/dist/lib/browser/intent-resolver-XAMO4BLV.mjs +0 -32
  141. package/dist/lib/browser/intent-resolver-XAMO4BLV.mjs.map +0 -7
  142. package/dist/lib/browser/meta.json +0 -1
  143. package/dist/lib/browser/react-surface-BY2DYCTH.mjs +0 -34
  144. package/dist/lib/browser/react-surface-BY2DYCTH.mjs.map +0 -7
  145. package/dist/lib/node-esm/ExplorerContainer-N3S5KSUX.mjs +0 -51
  146. package/dist/lib/node-esm/ExplorerContainer-N3S5KSUX.mjs.map +0 -7
  147. package/dist/lib/node-esm/chunk-3ODK27PU.mjs +0 -33
  148. package/dist/lib/node-esm/chunk-3ODK27PU.mjs.map +0 -7
  149. package/dist/lib/node-esm/chunk-4BY2XZET.mjs +0 -101
  150. package/dist/lib/node-esm/chunk-4BY2XZET.mjs.map +0 -7
  151. package/dist/lib/node-esm/chunk-CRSVAZNA.mjs +0 -190
  152. package/dist/lib/node-esm/chunk-CRSVAZNA.mjs.map +0 -7
  153. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  154. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  155. package/dist/lib/node-esm/chunk-NPIP4VEH.mjs +0 -11091
  156. package/dist/lib/node-esm/chunk-NPIP4VEH.mjs.map +0 -7
  157. package/dist/lib/node-esm/chunk-UXZM5VJB.mjs +0 -26
  158. package/dist/lib/node-esm/chunk-UXZM5VJB.mjs.map +0 -7
  159. package/dist/lib/node-esm/index.mjs +0 -113
  160. package/dist/lib/node-esm/index.mjs.map +0 -7
  161. package/dist/lib/node-esm/intent-resolver-YNS4MM2C.mjs +0 -33
  162. package/dist/lib/node-esm/intent-resolver-YNS4MM2C.mjs.map +0 -7
  163. package/dist/lib/node-esm/meta.json +0 -1
  164. package/dist/lib/node-esm/meta.mjs +0 -9
  165. package/dist/lib/node-esm/meta.mjs.map +0 -7
  166. package/dist/lib/node-esm/react-surface-7AAV7GBG.mjs +0 -35
  167. package/dist/lib/node-esm/react-surface-7AAV7GBG.mjs.map +0 -7
  168. package/dist/lib/node-esm/types/index.mjs +0 -12
  169. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  170. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  171. package/dist/types/src/components/ExplorerContainer.d.ts +0 -9
  172. package/dist/types/src/components/ExplorerContainer.d.ts.map +0 -1
  173. package/dist/types/src/components/Graph/D3ForceGraph.d.ts +0 -14
  174. package/dist/types/src/components/Graph/D3ForceGraph.d.ts.map +0 -1
  175. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts +0 -15
  176. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts.map +0 -1
  177. package/dist/types/src/components/Graph/adapter.d.ts.map +0 -1
  178. package/src/capabilities/intent-resolver.ts +0 -21
  179. package/src/components/ExplorerContainer.tsx +0 -54
  180. package/src/components/Graph/D3ForceGraph.stories.tsx +0 -80
  181. package/src/components/Graph/D3ForceGraph.tsx +0 -101
  182. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs +0 -0
  183. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs.map +0 -0
  184. /package/dist/lib/{browser/types → neutral}/index.mjs.map +0 -0
  185. /package/dist/lib/{browser → neutral}/meta.mjs.map +0 -0
  186. /package/dist/lib/{node-esm → neutral}/types/index.mjs.map +0 -0
@@ -0,0 +1,124 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { forceLink, forceManyBody } from 'd3';
6
+ import NativeForceGraph from 'force-graph';
7
+ import React, {
8
+ type ComponentPropsWithoutRef,
9
+ type Ref,
10
+ RefObject,
11
+ useCallback,
12
+ useEffect,
13
+ useRef,
14
+ useState,
15
+ } from 'react';
16
+ import { useResizeDetector } from 'react-resize-detector';
17
+
18
+ import { type SpaceGraphModel } from '@dxos/schema';
19
+ import { composable, composableProps } from '@dxos/ui-theme';
20
+
21
+ import { GraphAdapter } from './graph-adapter';
22
+
23
+ export type CanvasForceGraphProps = {
24
+ model?: SpaceGraphModel;
25
+ match?: RegExp;
26
+ };
27
+
28
+ /**
29
+ * More performance optimized version of the ForceGraph.
30
+ */
31
+ export const CanvasForceGraph = composable<HTMLDivElement, CanvasForceGraphProps>(
32
+ ({ model, match, onClick, ...props }, forwardedRef) => {
33
+ const { ref: resizeRef, width, height } = useResizeDetector({ refreshRate: 200 });
34
+ const setRef = useCallback(
35
+ (node: HTMLDivElement | null) => {
36
+ resizeRef(node);
37
+ assignRef(forwardedRef, node);
38
+ },
39
+ [resizeRef, forwardedRef],
40
+ );
41
+
42
+ const rootRef = useRef<HTMLDivElement>(null);
43
+ const forceGraph = useRef<NativeForceGraph>(null);
44
+
45
+ const [data, setData] = useState<GraphAdapter>();
46
+ useEffect(() => {
47
+ return model?.subscribe((model) => setData(new GraphAdapter(model.graph)));
48
+ }, [model]);
49
+
50
+ useEffect(() => {
51
+ if (rootRef.current) {
52
+ // https://github.com/vasturiano/force-graph
53
+ // https://github.com/vasturiano/3d-force-graph
54
+ forceGraph.current = new NativeForceGraph(rootRef.current)
55
+ // https://github.com/vasturiano/force-graph?tab=readme-ov-file#node-styling
56
+ .nodeRelSize(6)
57
+ .nodeLabel((node: any) => (node.type === 'schema' ? node.data.typename : (node.data.label ?? node.id)))
58
+ .nodeAutoColorBy((node: any) => (node.type === 'schema' ? 'schema' : node.data.typename))
59
+
60
+ // https://github.com/vasturiano/force-graph?tab=readme-ov-file#link-styling
61
+ .linkAutoColorBy((link: any) => link.type);
62
+ }
63
+
64
+ return () => {
65
+ forceGraph.current?.pauseAnimation().graphData({ nodes: [], links: [] });
66
+ forceGraph.current = null;
67
+ };
68
+ }, []);
69
+
70
+ useEffect(() => {
71
+ if (!data || !width || !height || !forceGraph.current) {
72
+ return;
73
+ }
74
+
75
+ // https://github.com/vasturiano/force-graph?tab=readme-ov-file#container-layout
76
+ forceGraph.current
77
+ .pauseAnimation()
78
+ .width(width)
79
+ .height(height)
80
+ .onEngineStop(() => handleZoomToFit())
81
+ .onNodeClick((node: any) => {
82
+ forceGraph.current?.emitParticle(node);
83
+ })
84
+
85
+ // https://github.com/vasturiano/force-graph?tab=readme-ov-file#force-engine-d3-force-configuration
86
+ // .d3Force('center', forceCenter().strength(0.9))
87
+ .d3Force('link', forceLink().distance(160).strength(0.5))
88
+ .d3Force('charge', forceManyBody().strength(-30))
89
+
90
+ .graphData(data)
91
+ .warmupTicks(100)
92
+ .cooldownTime(1_000)
93
+ .resumeAnimation();
94
+ }, [data, width, height]);
95
+
96
+ const handleZoomToFit = () => {
97
+ forceGraph.current?.zoomToFit(400, 40);
98
+ };
99
+
100
+ const handleClick = useCallback<NonNullable<ComponentPropsWithoutRef<'div'>['onClick']>>(
101
+ (event) => {
102
+ onClick?.(event);
103
+ if (!event.defaultPrevented) {
104
+ handleZoomToFit();
105
+ }
106
+ },
107
+ [onClick],
108
+ );
109
+
110
+ return (
111
+ <div {...composableProps(props, { classNames: 'relative grow' })} onClick={handleClick} ref={setRef}>
112
+ <div ref={rootRef} className='absolute inset-0' />
113
+ </div>
114
+ );
115
+ },
116
+ );
117
+
118
+ const assignRef = <T,>(ref: Ref<T> | undefined, value: T | null): void => {
119
+ if (typeof ref === 'function') {
120
+ ref(value);
121
+ } else if (ref) {
122
+ (ref as RefObject<T | null>).current = value;
123
+ }
124
+ };
@@ -3,68 +3,103 @@
3
3
  //
4
4
 
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
- import React, { useState } from 'react';
6
+ import * as Effect from 'effect/Effect';
7
+ import React, { useCallback, useMemo } from 'react';
7
8
 
8
- import { Type } from '@dxos/echo';
9
- import { faker } from '@dxos/random';
10
- import { useClient } from '@dxos/react-client';
11
- import { type Space } from '@dxos/react-client/echo';
12
- import { withClientProvider } from '@dxos/react-client/testing';
13
- import { useAsyncEffect } from '@dxos/react-ui';
14
- import { withTheme } from '@dxos/react-ui/testing';
15
- import { View } from '@dxos/schema';
9
+ import { withPluginManager } from '@dxos/app-framework/testing';
10
+ import { Obj, Type, View } from '@dxos/echo';
11
+ import { SelectionModel } from '@dxos/graph';
12
+ import { ClientPlugin } from '@dxos/plugin-client/plugin';
13
+ import { initializeIdentity } from '@dxos/plugin-client/testing';
14
+ import { PreviewPlugin } from '@dxos/plugin-preview/testing';
15
+ import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
16
+ import { random } from '@dxos/random';
17
+ import { useSpaces } from '@dxos/react-client/echo';
18
+ import { DxAnchorActivate } from '@dxos/react-ui';
19
+ import { type GraphProps, type GraphLayoutNode } from '@dxos/react-ui-graph';
20
+ import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
21
+ import { type SpaceGraphEdge, type SpaceGraphNode, ViewModel } from '@dxos/schema';
16
22
  import { type ValueGenerator } from '@dxos/schema/testing';
17
- import { render } from '@dxos/storybook-utils';
18
- import { HasRelationship, Organization, Person, Project } from '@dxos/types';
23
+ import { HasRelationship, Organization, Person, Pipeline } from '@dxos/types';
19
24
 
20
- import { useGraphModel } from '../../hooks';
21
- import { Graph } from '../../types';
25
+ import { useGraphModel } from '#hooks';
26
+ import { Graph } from '#types';
22
27
 
23
28
  import { ForceGraph } from './ForceGraph';
24
29
  import { generate } from './testing';
25
30
 
26
- const generator = faker as any as ValueGenerator;
31
+ const generator = random as any as ValueGenerator;
27
32
 
28
- faker.seed(1);
33
+ random.seed(1);
29
34
 
30
35
  const DefaultStory = () => {
31
- const client = useClient();
32
- const [space, setSpace] = useState<Space>();
33
- const [graph, setGraph] = useState<Graph.Graph>();
36
+ const [space] = useSpaces();
37
+ const model = useGraphModel(space);
34
38
 
35
- useAsyncEffect(async () => {
36
- const space = client.spaces.default;
37
- void generate(space, generator);
38
- const { view } = await View.makeFromSpace({ space, typename: Type.getTypename(Graph.Graph) });
39
- const graph = Graph.make({ name: 'Test', view });
40
- space.db.add(graph);
41
- setSpace(space);
42
- setGraph(graph);
43
- }, [client]);
39
+ const selection = useMemo(() => new SelectionModel({ mode: 'single' }), []);
44
40
 
45
- const model = useGraphModel(space);
46
- if (!model || !space || !graph) {
47
- return null;
41
+ const handleInspect = useCallback<NonNullable<GraphProps<SpaceGraphNode, SpaceGraphEdge>['onInspect']>>(
42
+ (node: GraphLayoutNode<SpaceGraphNode>, event) => {
43
+ const obj = node.data?.data?.object;
44
+ if (!obj) {
45
+ return;
46
+ }
47
+ const dxn = Obj.getDXN(obj)?.toString();
48
+ if (!dxn) {
49
+ return;
50
+ }
51
+ const target = event.target as HTMLElement;
52
+ target.dispatchEvent(
53
+ new DxAnchorActivate({
54
+ dxn,
55
+ label: Obj.getLabel(obj) ?? dxn,
56
+ trigger: target,
57
+ kind: 'card',
58
+ }),
59
+ );
60
+ },
61
+ [],
62
+ );
63
+
64
+ if (!space || !model) {
65
+ return <Loading data={{ space: !!space, model: !!model }} />;
48
66
  }
49
67
 
50
- return <ForceGraph model={model} />;
68
+ return <ForceGraph model={model} selection={selection} onInspect={handleInspect} />;
51
69
  };
52
70
 
53
71
  const meta = {
54
- title: 'plugins/plugin-explorer/ForceGraph',
72
+ title: 'plugins/plugin-explorer/components/ForceGraph',
55
73
  component: ForceGraph,
56
- render: render(DefaultStory),
74
+ render: DefaultStory,
57
75
  decorators: [
58
- withTheme,
59
- withClientProvider({
60
- createSpace: true,
61
- types: [
62
- Graph.Graph,
63
- View.View,
64
- HasRelationship.HasRelationship,
65
- Organization.Organization,
66
- Project.Project,
67
- Person.Person,
76
+ withTheme(),
77
+ withLayout({ layout: 'fullscreen' }),
78
+ withPluginManager({
79
+ plugins: [
80
+ ...corePlugins(),
81
+ StorybookPlugin({}),
82
+ ClientPlugin({
83
+ types: [
84
+ Graph.Graph,
85
+ View.View,
86
+ HasRelationship.HasRelationship,
87
+ Organization.Organization,
88
+ Pipeline.Pipeline,
89
+ Person.Person,
90
+ ],
91
+ onClientInitialized: ({ client }) =>
92
+ Effect.gen(function* () {
93
+ const { personalSpace } = yield* initializeIdentity(client);
94
+ yield* Effect.promise(() => generate(personalSpace, generator));
95
+ const { view } = yield* Effect.promise(() =>
96
+ ViewModel.makeFromDatabase({ db: personalSpace.db, typename: Type.getTypename(Graph.Graph) }),
97
+ );
98
+ personalSpace.db.add(Graph.make({ name: 'Test', view }));
99
+ yield* Effect.promise(() => personalSpace.db.flush({ indexes: true }));
100
+ }),
101
+ }),
102
+ PreviewPlugin(),
68
103
  ],
69
104
  }),
70
105
  ],
@@ -2,91 +2,110 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { forceLink, forceManyBody } from 'd3';
6
- import NativeForceGraph from 'force-graph';
7
- import React, { type FC, useEffect, useRef, useState } from 'react';
8
- import { useResizeDetector } from 'react-resize-detector';
9
-
10
- import { type SearchResult, filterObjectsSync } from '@dxos/plugin-search';
11
- import { type SpaceGraphModel } from '@dxos/schema';
12
-
13
- import { GraphAdapter } from './adapter';
5
+ import { Atom, useAtomValue } from '@effect-atom/atom-react';
6
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
+
8
+ import { Obj } from '@dxos/echo';
9
+ import { SelectionModel } from '@dxos/graph';
10
+ import {
11
+ type GraphController,
12
+ GraphForceProjector,
13
+ type GraphLayoutNode,
14
+ type GraphProps,
15
+ SVG,
16
+ type SVGContext,
17
+ } from '@dxos/react-ui-graph';
18
+ import { type SpaceGraphEdge, type SpaceGraphModel, type SpaceGraphNode } from '@dxos/schema';
19
+ import { composable, composableProps, getHashStyles } from '@dxos/ui-theme';
20
+ import '@dxos/react-ui-graph/styles/graph.css';
21
+
22
+ const EMPTY_ATOM = Atom.make<{ nodes: SpaceGraphNode[]; edges: SpaceGraphEdge[] }>({ nodes: [], edges: [] });
14
23
 
15
24
  export type ForceGraphProps = {
16
25
  model?: SpaceGraphModel;
17
- match?: RegExp;
18
- };
19
-
20
- export const ForceGraph: FC<ForceGraphProps> = ({ model, match }) => {
21
- const { ref, width, height } = useResizeDetector({ refreshRate: 200 });
22
- const rootRef = useRef<HTMLDivElement>(null);
23
- const forceGraph = useRef<NativeForceGraph>(null);
24
-
25
- const filteredRef = useRef<SearchResult[]>([]);
26
- filteredRef.current = filterObjectsSync(model?.objects ?? [], match);
27
-
28
- const [data, setData] = useState<GraphAdapter>();
29
- useEffect(() => {
30
- return model?.subscribe((model) => {
31
- setData(new GraphAdapter(model.graph));
32
- });
33
- }, [model]);
34
-
35
- useEffect(() => {
36
- if (rootRef.current) {
37
- // https://github.com/vasturiano/force-graph
38
- // https://github.com/vasturiano/3d-force-graph
39
- forceGraph.current = new NativeForceGraph(rootRef.current)
40
- // https://github.com/vasturiano/force-graph?tab=readme-ov-file#node-styling
41
- .nodeRelSize(6)
42
- .nodeLabel((node: any) => (node.type === 'schema' ? node.data.typename : (node.data.label ?? node.id)))
43
- .nodeAutoColorBy((node: any) => (node.type === 'schema' ? 'schema' : node.data.typename))
44
-
45
- // https://github.com/vasturiano/force-graph?tab=readme-ov-file#link-styling
46
- .linkAutoColorBy((link: any) => link.type);
47
- }
48
-
49
- return () => {
50
- forceGraph.current?.pauseAnimation().graphData({ nodes: [], links: [] });
51
- forceGraph.current = null;
52
- };
53
- }, []);
54
-
55
- useEffect(() => {
56
- if (!data || !width || !height || !forceGraph.current) {
57
- return;
58
- }
59
-
60
- // https://github.com/vasturiano/force-graph?tab=readme-ov-file#container-layout
61
- forceGraph.current
62
- .pauseAnimation()
63
- .width(width)
64
- .height(height)
65
- .onEngineStop(() => {
66
- handleZoomToFit();
67
- })
68
- .onNodeClick((node: any) => {
69
- forceGraph.current?.emitParticle(node);
70
- })
71
-
72
- // https://github.com/vasturiano/force-graph?tab=readme-ov-file#force-engine-d3-force-configuration
73
- // .d3Force('center', forceCenter().strength(0.9))
74
- .d3Force('link', forceLink().distance(160).strength(0.5))
75
- .d3Force('charge', forceManyBody().strength(-30))
76
-
77
- .graphData(data)
78
- .warmupTicks(100)
79
- .cooldownTime(1_000)
80
- .resumeAnimation();
81
- }, [data, width, height, forceGraph.current]);
82
-
83
- const handleZoomToFit = () => {
84
- forceGraph.current?.zoomToFit(400, 40);
85
- };
86
-
87
- return (
88
- <div ref={ref} className='relative grow' onClick={handleZoomToFit}>
89
- <div ref={rootRef} className='absolute inset-0' />
90
- </div>
91
- );
92
- };
26
+ grid?: boolean;
27
+ selection?: SelectionModel;
28
+ onInspect?: GraphProps<SpaceGraphNode, SpaceGraphEdge>['onInspect'];
29
+ } & Pick<GraphProps, 'drag'>;
30
+
31
+ export const ForceGraph = composable<HTMLDivElement, ForceGraphProps>(
32
+ ({ model, selection: selectionProp, grid, drag, onInspect, ...props }, forwardedRef) => {
33
+ // TODO(wittjosiah): This should go into Graph.tsx but for some reason doesn't work.
34
+ useAtomValue(model?.graphAtom ?? EMPTY_ATOM);
35
+
36
+ const graph = useRef<GraphController>(null);
37
+ const selection = useMemo(() => selectionProp ?? new SelectionModel(), [selectionProp]);
38
+ useEffect(() => {
39
+ const unsubscribe = selection.subscribe(() => graph.current?.repaint());
40
+ return unsubscribe;
41
+ }, [selection]);
42
+
43
+ const svgRef = useRef<SVGContext>(null);
44
+ const [projector, setProjector] = useState<GraphForceProjector>();
45
+ useEffect(() => {
46
+ if (svgRef.current) {
47
+ setProjector(
48
+ new GraphForceProjector(svgRef.current, {
49
+ attributes: {
50
+ // TODO(burdon): Check type (currently assumes Employee property).
51
+ // Edge shouldn't contribute to force if it's not active.
52
+ linkForce: (edge) => edge.data?.object?.active !== false,
53
+ },
54
+ forces: {
55
+ point: {
56
+ strength: 0.01,
57
+ },
58
+ },
59
+ }),
60
+ );
61
+ }
62
+ // SVG.Graph owns projector start/stop; nothing to clean up here.
63
+ }, []);
64
+
65
+ const handleSelect = useCallback<NonNullable<GraphProps['onSelect']>>(
66
+ (node) => {
67
+ if (selection.contains(node.id)) {
68
+ selection.remove(node.id);
69
+ } else {
70
+ selection.add(node.id);
71
+ }
72
+ },
73
+ [selection],
74
+ );
75
+
76
+ return (
77
+ <div {...composableProps(props, { classNames: 'dx-container' })} ref={forwardedRef}>
78
+ <SVG.Root ref={svgRef}>
79
+ <SVG.Markers />
80
+ {grid && <SVG.Grid axis />}
81
+ <SVG.Zoom extent={[1 / 2, 2]}>
82
+ <SVG.Graph<SpaceGraphNode, SpaceGraphEdge>
83
+ drag={drag}
84
+ ref={graph}
85
+ model={model}
86
+ projector={projector}
87
+ labels={{
88
+ text: (node) => node.data?.data.label ?? node.id,
89
+ }}
90
+ attributes={{
91
+ node: (node: GraphLayoutNode<SpaceGraphNode>) => {
92
+ const obj = node.data?.data.object;
93
+ return {
94
+ data: {
95
+ color: getHashStyles(obj && Obj.getTypename(obj))?.hue,
96
+ },
97
+ classes: {
98
+ 'dx-selected': selection.contains(node.id),
99
+ },
100
+ };
101
+ },
102
+ }}
103
+ onSelect={handleSelect}
104
+ onInspect={onInspect}
105
+ />
106
+ </SVG.Zoom>
107
+ </SVG.Root>
108
+ </div>
109
+ );
110
+ },
111
+ );
@@ -25,19 +25,25 @@ export class GraphAdapter implements GraphData {
25
25
  private readonly _nodes: GraphNode[] = [];
26
26
  private readonly _links: GraphLink[] = [];
27
27
 
28
- constructor(private readonly graph: Graph) {
29
- this._nodes = graph.nodes.map((node) => ({
28
+ constructor(private readonly graph: Graph.Any) {
29
+ this._nodes = graph.nodes.map((node: Graph.Node.Any) => ({
30
30
  id: node.id,
31
31
  type: node.type,
32
32
  data: node.data,
33
33
  }));
34
34
 
35
- this._links = graph.edges.map((edge) => ({
36
- type: edge.type,
37
- source: edge.source,
38
- target: edge.target,
39
- data: edge.data,
40
- }));
35
+ // Build a set of node IDs for efficient lookup.
36
+ const nodeIds = new Set(this._nodes.map((node) => node.id));
37
+
38
+ // Filter out edges where source or target node doesn't exist.
39
+ this._links = graph.edges
40
+ .filter((edge: Graph.Edge.Any) => nodeIds.has(edge.source) && nodeIds.has(edge.target))
41
+ .map((edge: Graph.Edge.Any) => ({
42
+ type: edge.type,
43
+ source: edge.source,
44
+ target: edge.target,
45
+ data: edge.data,
46
+ }));
41
47
  }
42
48
 
43
49
  get nodes() {
@@ -2,5 +2,5 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './D3ForceGraph';
5
+ export * from './CanvasForceGraph';
6
6
  export * from './ForceGraph';
@@ -5,14 +5,14 @@
5
5
  import { type Space } from '@dxos/client/echo';
6
6
  import { type Obj, Query, Relation } from '@dxos/echo';
7
7
  import { type TypeSpec, type ValueGenerator, createObjectFactory } from '@dxos/schema/testing';
8
- import { HasRelationship, Organization, Person, Project } from '@dxos/types';
8
+ import { HasRelationship, Organization, Person, Pipeline } from '@dxos/types';
9
9
  import { range } from '@dxos/util';
10
10
 
11
- const getObject = (objects: Obj.Any[]) => objects[Math.floor(Math.random() * objects.length)];
11
+ const getObject = (objects: Obj.Unknown[]) => objects[Math.floor(Math.random() * objects.length)];
12
12
 
13
13
  const defaultTypes: TypeSpec[] = [
14
14
  { type: Organization.Organization, count: 5 },
15
- { type: Project.Project, count: 5 },
15
+ { type: Pipeline.Pipeline, count: 5 },
16
16
  { type: Person.Person, count: 10 },
17
17
  ];
18
18
 
@@ -3,50 +3,58 @@
3
3
  //
4
4
 
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
- import React, { useEffect, useState } from 'react';
7
-
8
- import { faker } from '@dxos/random';
9
- import { useClient } from '@dxos/react-client';
10
- import { type ClientRepeatedComponentProps, ClientRepeater } from '@dxos/react-client/testing';
11
- import { withTheme } from '@dxos/react-ui/testing';
12
-
6
+ import * as Effect from 'effect/Effect';
7
+ import React from 'react';
8
+
9
+ import { withPluginManager } from '@dxos/app-framework/testing';
10
+ import { Filter } from '@dxos/echo';
11
+ import { ClientPlugin } from '@dxos/plugin-client/plugin';
12
+ import { initializeIdentity } from '@dxos/plugin-client/testing';
13
+ import { corePlugins } from '@dxos/plugin-testing';
14
+ import { random } from '@dxos/random';
15
+ import { useQuery, useSpaces } from '@dxos/react-client/echo';
16
+ import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
17
+
18
+ import { createTree } from './testing';
13
19
  import { Tree, type TreeComponentProps } from './Tree';
14
- import { Tree as TreeModel, TreeType } from './types';
15
-
16
- // TODO(burdon): Storybook for Graph/Tree/Plot (generics); incl. GraphModel.
17
- // TODO(burdon): Type for all Explorer components (Space, Object, Query, etc.) incl.
20
+ import { TreeType } from './types';
18
21
 
19
- faker.seed(1);
22
+ random.seed(1);
20
23
 
21
- type ComponentProps = ClientRepeatedComponentProps & { type?: TreeComponentProps<any>['variant'] };
24
+ type StoryArgs = { variant?: TreeComponentProps<any>['variant'] };
22
25
 
23
- const Component = ({ type }: ComponentProps) => {
24
- const client = useClient();
25
- const space = client.spaces.default;
26
- const [object, setObject] = useState<TreeType>();
27
- useEffect(() => {
28
- setTimeout(() => {
29
- const tree = space.db.add(TreeModel.create());
30
- setObject(tree);
31
- });
32
- }, []);
33
-
34
- if (!object) {
35
- return null;
26
+ const DefaultStory = ({ variant }: StoryArgs) => {
27
+ const [space] = useSpaces();
28
+ const [tree] = useQuery(space?.db, Filter.type(TreeType));
29
+ if (!space || !tree) {
30
+ return <Loading data={{ space: !!space, tree: !!tree }} />;
36
31
  }
37
32
 
38
- return <Tree space={space} selected={object?.id} variant={type} />;
39
- };
40
-
41
- const DefaultStory = () => {
42
- return <ClientRepeater component={Component} types={[TreeType]} createSpace />;
33
+ return <Tree space={space} selected={tree.id} variant={variant} />;
43
34
  };
44
35
 
45
36
  const meta = {
46
- title: 'plugins/plugin-explorer/Tree',
37
+ title: 'plugins/plugin-explorer/components/Tree',
47
38
  component: Tree as any,
48
39
  render: DefaultStory,
49
- decorators: [withTheme],
40
+ decorators: [
41
+ withTheme(),
42
+ withLayout({ layout: 'fullscreen' }),
43
+ withPluginManager({
44
+ plugins: [
45
+ ...corePlugins(),
46
+ ClientPlugin({
47
+ types: [TreeType],
48
+ onClientInitialized: ({ client }) =>
49
+ Effect.gen(function* () {
50
+ const { personalSpace } = yield* initializeIdentity(client);
51
+ const tree = createTree([3, [2, 4], [1, 3]]).tree;
52
+ personalSpace.db.add(tree);
53
+ }),
54
+ }),
55
+ ],
56
+ }),
57
+ ],
50
58
  parameters: {
51
59
  layout: 'fullscreen',
52
60
  },
@@ -58,18 +66,18 @@ type Story = StoryObj<typeof meta>;
58
66
 
59
67
  export const Tidy: Story = {
60
68
  args: {
61
- type: 'tidy',
69
+ variant: 'tidy',
62
70
  },
63
71
  };
64
72
 
65
73
  export const Radial: Story = {
66
74
  args: {
67
- type: 'radial',
75
+ variant: 'radial',
68
76
  },
69
77
  };
70
78
 
71
79
  export const Edge: Story = {
72
80
  args: {
73
- type: 'edge',
81
+ variant: 'edge',
74
82
  },
75
83
  };