@dxos/plugin-explorer 0.8.4-main.c85a9c8dae → 0.8.4-main.dfabb4ec29

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 (156) hide show
  1. package/dist/lib/{browser/ExplorerContainer-4RB2TY3G.mjs → neutral/ExplorerContainer-5TOK2ZEY.mjs} +8 -16
  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/{browser/chunk-4NFGHCGO.mjs → neutral/chunk-5X5ATGCS.mjs} +11 -22
  8. package/dist/lib/neutral/chunk-5X5ATGCS.mjs.map +7 -0
  9. package/dist/lib/{browser/chunk-YNQF4CPY.mjs → neutral/chunk-HPIS2WXY.mjs} +2 -2
  10. package/dist/lib/{browser/chunk-YNQF4CPY.mjs.map → neutral/chunk-HPIS2WXY.mjs.map} +3 -3
  11. package/dist/lib/{browser/chunk-6AZY4CDH.mjs → neutral/components/index.mjs} +144 -148
  12. package/dist/lib/{node-esm/chunk-DOXAIJEC.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 -1
  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 +1 -0
  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 -1
  40. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  41. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  42. package/dist/types/src/components/Chart/Chart.d.ts.map +1 -1
  43. package/dist/types/src/components/Chart/Chart.stories.d.ts +4 -1
  44. package/dist/types/src/components/Chart/Chart.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  46. package/dist/types/src/components/Globe/Globe.stories.d.ts +5 -2
  47. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  48. package/dist/types/src/components/Graph/CanvasForceGraph.d.ts +13 -0
  49. package/dist/types/src/components/Graph/CanvasForceGraph.d.ts.map +1 -0
  50. package/dist/types/src/components/Graph/CanvasForceGraph.stories.d.ts +17 -0
  51. package/dist/types/src/components/Graph/CanvasForceGraph.stories.d.ts.map +1 -0
  52. package/dist/types/src/components/Graph/ForceGraph.d.ts +12 -5
  53. package/dist/types/src/components/Graph/ForceGraph.d.ts.map +1 -1
  54. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts +4 -2
  55. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts.map +1 -1
  56. package/dist/types/src/components/Graph/{adapter.d.ts → graph-adapter.d.ts} +1 -1
  57. package/dist/types/src/components/Graph/graph-adapter.d.ts.map +1 -0
  58. package/dist/types/src/components/Graph/index.d.ts +1 -1
  59. package/dist/types/src/components/Graph/index.d.ts.map +1 -1
  60. package/dist/types/src/components/Graph/testing.d.ts.map +1 -1
  61. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  62. package/dist/types/src/components/Tree/Tree.stories.d.ts +5 -1
  63. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  64. package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts.map +1 -1
  65. package/dist/types/src/components/Tree/layout/RadialTree.d.ts.map +1 -1
  66. package/dist/types/src/components/Tree/layout/TidyTree.d.ts.map +1 -1
  67. package/dist/types/src/components/Tree/testing/generator.d.ts.map +1 -1
  68. package/dist/types/src/components/Tree/types/tree.d.ts +6 -6
  69. package/dist/types/src/components/Tree/types/tree.d.ts.map +1 -1
  70. package/dist/types/src/components/Tree/types/types.d.ts.map +1 -1
  71. package/dist/types/src/components/plot.d.ts.map +1 -1
  72. package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts +3 -3
  73. package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts.map +1 -1
  74. package/dist/types/src/containers/ExplorerContainer/index.d.ts +1 -2
  75. package/dist/types/src/containers/ExplorerContainer/index.d.ts.map +1 -1
  76. package/dist/types/src/hooks/useGraphModel.d.ts.map +1 -1
  77. package/dist/types/src/index.d.ts +1 -3
  78. package/dist/types/src/index.d.ts.map +1 -1
  79. package/dist/types/src/plugin.d.ts +3 -0
  80. package/dist/types/src/plugin.d.ts.map +1 -0
  81. package/dist/types/src/translations.d.ts +28 -26
  82. package/dist/types/src/translations.d.ts.map +1 -1
  83. package/dist/types/src/types/ExplorerAction.d.ts.map +1 -1
  84. package/dist/types/src/types/Graph.d.ts +2 -9
  85. package/dist/types/src/types/Graph.d.ts.map +1 -1
  86. package/dist/types/tsconfig.tsbuildinfo +1 -1
  87. package/package.json +98 -57
  88. package/src/ExplorerPlugin.test.ts +26 -0
  89. package/src/ExplorerPlugin.tsx +7 -24
  90. package/src/capabilities/create-object.ts +36 -0
  91. package/src/capabilities/index.ts +4 -1
  92. package/src/capabilities/{react-surface/react-surface.tsx → react-surface.tsx} +10 -9
  93. package/src/components/Chart/Chart.stories.tsx +14 -21
  94. package/src/components/Globe/Globe.stories.tsx +17 -20
  95. package/src/components/Graph/CanvasForceGraph.stories.tsx +83 -0
  96. package/src/components/Graph/CanvasForceGraph.tsx +124 -0
  97. package/src/components/Graph/ForceGraph.stories.tsx +75 -44
  98. package/src/components/Graph/ForceGraph.tsx +104 -85
  99. package/src/components/Graph/index.ts +1 -1
  100. package/src/components/Tree/Tree.stories.tsx +43 -36
  101. package/src/components/Tree/testing/generator.ts +1 -1
  102. package/src/components/Tree/types/tree.test.ts +3 -4
  103. package/src/components/Tree/types/tree.ts +13 -13
  104. package/src/containers/ExplorerContainer/ExplorerContainer.tsx +7 -9
  105. package/src/containers/ExplorerContainer/index.ts +1 -3
  106. package/src/hooks/useGraphModel.ts +14 -10
  107. package/src/index.ts +1 -4
  108. package/src/meta.ts +1 -1
  109. package/src/plugin.ts +9 -0
  110. package/src/translations.ts +14 -13
  111. package/src/types/ExplorerAction.ts +1 -2
  112. package/src/types/Graph.ts +8 -22
  113. package/src/typings.d.ts +8 -0
  114. package/dist/lib/browser/ExplorerContainer-4RB2TY3G.mjs.map +0 -7
  115. package/dist/lib/browser/chunk-4NFGHCGO.mjs.map +0 -7
  116. package/dist/lib/browser/chunk-6AZY4CDH.mjs.map +0 -7
  117. package/dist/lib/browser/index.mjs +0 -95
  118. package/dist/lib/browser/index.mjs.map +0 -7
  119. package/dist/lib/browser/meta.json +0 -1
  120. package/dist/lib/browser/react-surface-KAHVDMFX.mjs +0 -38
  121. package/dist/lib/browser/react-surface-KAHVDMFX.mjs.map +0 -7
  122. package/dist/lib/node-esm/ExplorerContainer-LCG425I7.mjs +0 -49
  123. package/dist/lib/node-esm/ExplorerContainer-LCG425I7.mjs.map +0 -7
  124. package/dist/lib/node-esm/chunk-DK77RB6M.mjs +0 -26
  125. package/dist/lib/node-esm/chunk-DK77RB6M.mjs.map +0 -7
  126. package/dist/lib/node-esm/chunk-DOXAIJEC.mjs +0 -11280
  127. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  128. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  129. package/dist/lib/node-esm/chunk-V42OFY7B.mjs +0 -85
  130. package/dist/lib/node-esm/chunk-V42OFY7B.mjs.map +0 -7
  131. package/dist/lib/node-esm/index.mjs +0 -96
  132. package/dist/lib/node-esm/index.mjs.map +0 -7
  133. package/dist/lib/node-esm/meta.json +0 -1
  134. package/dist/lib/node-esm/meta.mjs +0 -9
  135. package/dist/lib/node-esm/meta.mjs.map +0 -7
  136. package/dist/lib/node-esm/react-surface-7XILIUI4.mjs +0 -39
  137. package/dist/lib/node-esm/react-surface-7XILIUI4.mjs.map +0 -7
  138. package/dist/lib/node-esm/types/index.mjs +0 -11
  139. package/dist/types/src/capabilities/react-surface/index.d.ts +0 -3
  140. package/dist/types/src/capabilities/react-surface/index.d.ts.map +0 -1
  141. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +0 -1
  142. package/dist/types/src/components/Graph/D3ForceGraph.d.ts +0 -14
  143. package/dist/types/src/components/Graph/D3ForceGraph.d.ts.map +0 -1
  144. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts +0 -15
  145. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts.map +0 -1
  146. package/dist/types/src/components/Graph/adapter.d.ts.map +0 -1
  147. package/src/capabilities/react-surface/index.ts +0 -7
  148. package/src/components/Graph/D3ForceGraph.stories.tsx +0 -84
  149. package/src/components/Graph/D3ForceGraph.tsx +0 -102
  150. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs +0 -0
  151. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs.map +0 -0
  152. /package/dist/lib/{browser/types → neutral}/index.mjs.map +0 -0
  153. /package/dist/lib/{browser → neutral}/meta.mjs.map +0 -0
  154. /package/dist/lib/{node-esm → neutral}/types/index.mjs.map +0 -0
  155. /package/dist/types/src/capabilities/{react-surface/react-surface.d.ts → react-surface.d.ts} +0 -0
  156. /package/src/components/Graph/{adapter.ts → graph-adapter.ts} +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,72 +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 { View } from '@dxos/echo';
10
- import { faker } from '@dxos/random';
11
- import { useClient } from '@dxos/react-client';
12
- import { type Space } from '@dxos/react-client/echo';
13
- import { withClientProvider } from '@dxos/react-client/testing';
14
- import { useAsyncEffect } from '@dxos/react-ui';
15
- import { withLayout, withTheme } from '@dxos/react-ui/testing';
16
- import { ViewModel } 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';
17
22
  import { type ValueGenerator } from '@dxos/schema/testing';
18
- import { withRegistry } from '@dxos/storybook-utils';
19
- import { render } from '@dxos/storybook-utils';
20
23
  import { HasRelationship, Organization, Person, Pipeline } from '@dxos/types';
21
24
 
22
- import { useGraphModel } from '../../hooks';
23
- import { Graph } from '../../types';
25
+ import { useGraphModel } from '#hooks';
26
+ import { Graph } from '#types';
24
27
 
25
28
  import { ForceGraph } from './ForceGraph';
26
29
  import { generate } from './testing';
27
30
 
28
- const generator = faker as any as ValueGenerator;
31
+ const generator = random as any as ValueGenerator;
29
32
 
30
- faker.seed(1);
33
+ random.seed(1);
31
34
 
32
35
  const DefaultStory = () => {
33
- const client = useClient();
34
- const [space, setSpace] = useState<Space>();
35
- const [graph, setGraph] = useState<Graph.Graph>();
36
+ const [space] = useSpaces();
37
+ const model = useGraphModel(space);
36
38
 
37
- useAsyncEffect(async () => {
38
- const space = client.spaces.default;
39
- void generate(space, generator);
40
- const { view } = await ViewModel.makeFromDatabase({ db: space.db, typename: Type.getTypename(Graph.Graph) });
41
- const graph = Graph.make({ name: 'Test', view });
42
- space.db.add(graph);
43
- setSpace(space);
44
- setGraph(graph);
45
- }, [client]);
39
+ const selection = useMemo(() => new SelectionModel({ mode: 'single' }), []);
46
40
 
47
- const model = useGraphModel(space);
48
- if (!model || !space || !graph) {
49
- 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 }} />;
50
66
  }
51
67
 
52
- return <ForceGraph model={model} />;
68
+ return <ForceGraph model={model} selection={selection} onInspect={handleInspect} />;
53
69
  };
54
70
 
55
71
  const meta = {
56
72
  title: 'plugins/plugin-explorer/components/ForceGraph',
57
73
  component: ForceGraph,
58
- render: render(DefaultStory),
74
+ render: DefaultStory,
59
75
  decorators: [
60
- withRegistry,
61
76
  withTheme(),
62
- withLayout(),
63
- withClientProvider({
64
- createSpace: true,
65
- types: [
66
- Graph.Graph,
67
- View.View,
68
- HasRelationship.HasRelationship,
69
- Organization.Organization,
70
- Pipeline.Pipeline,
71
- Person.Person,
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(),
72
103
  ],
73
104
  }),
74
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
+ );
@@ -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';
@@ -3,51 +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 { withLayout, withTheme } from '@dxos/react-ui/testing';
12
- import { withRegistry } from '@dxos/storybook-utils';
13
-
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';
14
19
  import { Tree, type TreeComponentProps } from './Tree';
15
- import { Tree as TreeModel, TreeType } from './types';
16
-
17
- // TODO(burdon): Storybook for Graph/Tree/Plot (generics); incl. GraphModel.
18
- // TODO(burdon): Type for all Explorer components (Space, Object, Query, etc.) incl.
20
+ import { TreeType } from './types';
19
21
 
20
- faker.seed(1);
22
+ random.seed(1);
21
23
 
22
- type ComponentProps = ClientRepeatedComponentProps & { type?: TreeComponentProps<any>['variant'] };
24
+ type StoryArgs = { variant?: TreeComponentProps<any>['variant'] };
23
25
 
24
- const Component = ({ type }: ComponentProps) => {
25
- const client = useClient();
26
- const space = client.spaces.default;
27
- const [object, setObject] = useState<TreeType>();
28
- useEffect(() => {
29
- setTimeout(() => {
30
- const tree = space.db.add(TreeModel.create());
31
- setObject(tree);
32
- });
33
- }, []);
34
-
35
- if (!object) {
36
- 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 }} />;
37
31
  }
38
32
 
39
- return <Tree space={space} selected={object?.id} variant={type} />;
40
- };
41
-
42
- const DefaultStory = () => {
43
- return <ClientRepeater component={Component} types={[TreeType]} createSpace />;
33
+ return <Tree space={space} selected={tree.id} variant={variant} />;
44
34
  };
45
35
 
46
36
  const meta = {
47
37
  title: 'plugins/plugin-explorer/components/Tree',
48
38
  component: Tree as any,
49
39
  render: DefaultStory,
50
- decorators: [withRegistry, withTheme(), withLayout()],
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
+ ],
51
58
  parameters: {
52
59
  layout: 'fullscreen',
53
60
  },
@@ -59,18 +66,18 @@ type Story = StoryObj<typeof meta>;
59
66
 
60
67
  export const Tidy: Story = {
61
68
  args: {
62
- type: 'tidy',
69
+ variant: 'tidy',
63
70
  },
64
71
  };
65
72
 
66
73
  export const Radial: Story = {
67
74
  args: {
68
- type: 'radial',
75
+ variant: 'radial',
69
76
  },
70
77
  };
71
78
 
72
79
  export const Edge: Story = {
73
80
  args: {
74
- type: 'edge',
81
+ variant: 'edge',
75
82
  },
76
83
  };
@@ -16,7 +16,7 @@ const random = (min: number, max: number) => Math.floor(Math.random() * (max - m
16
16
  */
17
17
  export const createTree = (spec: NumberOrNumberArray[] = [], createText?: () => string): Tree => {
18
18
  const tree = new Tree();
19
- Obj.change(tree.tree, () => {
19
+ Obj.update(tree.tree, () => {
20
20
  tree.root.data = { text: 'root' };
21
21
  });
22
22
 
@@ -5,14 +5,13 @@
5
5
  import { describe, test } from 'vitest';
6
6
 
7
7
  import { Obj, Ref } from '@dxos/echo';
8
- import { faker } from '@dxos/random';
8
+ import { random } from '@dxos/random';
9
9
  import { Task } from '@dxos/types';
10
10
 
11
11
  import { createTree } from '../testing';
12
-
13
12
  import { type Tree } from './tree';
14
13
 
15
- faker.seed(0);
14
+ random.seed(0);
16
15
 
17
16
  const print = (tree: Tree) => {
18
17
  let count = 0;
@@ -129,7 +128,7 @@ describe('tree', () => {
129
128
 
130
129
  const tree = createTree();
131
130
  const node = tree.addNode(tree.root);
132
- Obj.change(tree.tree, () => {
131
+ Obj.update(tree.tree, () => {
133
132
  node.ref = Ref.make(task);
134
133
  });
135
134
  });