@dxos/plugin-explorer 0.8.2-main.fbd8ed0 → 0.8.2-staging.42af850
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/lib/browser/ExplorerContainer-BBZ54DJS.mjs +37 -0
- package/dist/lib/browser/ExplorerContainer-BBZ54DJS.mjs.map +7 -0
- package/dist/lib/browser/{chunk-JL7ZGOFE.mjs → chunk-73GQ46YO.mjs} +415 -155
- package/dist/lib/{node-esm/chunk-MUHCE377.mjs.map → browser/chunk-73GQ46YO.mjs.map} +4 -4
- package/dist/lib/browser/{chunk-HKQSZX7S.mjs → chunk-73YTQHOT.mjs} +3 -3
- package/dist/lib/browser/{chunk-HKQSZX7S.mjs.map → chunk-73YTQHOT.mjs.map} +1 -1
- package/dist/lib/browser/chunk-M2BGAY6H.mjs +177 -0
- package/dist/lib/browser/chunk-M2BGAY6H.mjs.map +7 -0
- package/dist/lib/browser/{chunk-Z2SDLMQM.mjs → chunk-OBAFAA5V.mjs} +3 -3
- package/dist/lib/browser/{chunk-Z2SDLMQM.mjs.map → chunk-OBAFAA5V.mjs.map} +1 -1
- package/dist/lib/browser/chunk-SLB2F5AO.mjs +30 -0
- package/dist/lib/browser/chunk-SLB2F5AO.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +15 -11
- package/dist/lib/browser/index.mjs.map +1 -1
- package/dist/lib/browser/{intent-resolver-7JF4DU2C.mjs → intent-resolver-FJDVBDE3.mjs} +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/meta.mjs +1 -1
- package/dist/lib/browser/{react-surface-JLE6GLG6.mjs → react-surface-H3YDMXAQ.mjs} +5 -5
- package/dist/lib/browser/types/index.mjs +2 -2
- package/dist/lib/node/{ExplorerContainer-4RMWCWRX.cjs → ExplorerContainer-MVP2AM7R.cjs} +24 -16
- package/dist/lib/node/ExplorerContainer-MVP2AM7R.cjs.map +7 -0
- package/dist/lib/node/chunk-4T4LCT5R.cjs +52 -0
- package/dist/lib/node/chunk-4T4LCT5R.cjs.map +7 -0
- package/dist/lib/node/{chunk-R4TOGRPE.cjs → chunk-72H5HBTK.cjs} +414 -153
- package/dist/lib/node/{chunk-R4TOGRPE.cjs.map → chunk-72H5HBTK.cjs.map} +4 -4
- package/dist/lib/node/{chunk-VB3QE6XY.cjs → chunk-BCDVG2CH.cjs} +6 -6
- package/dist/lib/node/{chunk-VB3QE6XY.cjs.map → chunk-BCDVG2CH.cjs.map} +1 -1
- package/dist/lib/node/{chunk-6R5P3UVS.cjs → chunk-MLRYW4WQ.cjs} +7 -7
- package/dist/lib/node/{chunk-6R5P3UVS.cjs.map → chunk-MLRYW4WQ.cjs.map} +1 -1
- package/dist/lib/node/chunk-NELWWGBU.cjs +204 -0
- package/dist/lib/node/chunk-NELWWGBU.cjs.map +7 -0
- package/dist/lib/node/index.cjs +34 -31
- package/dist/lib/node/index.cjs.map +1 -1
- package/dist/lib/node/{intent-resolver-VDAHQEE7.cjs → intent-resolver-DRT67ZU4.cjs} +8 -8
- package/dist/lib/node/meta.cjs +3 -3
- package/dist/lib/node/meta.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/{react-surface-F66QYWDR.cjs → react-surface-6ESLSM33.cjs} +11 -11
- package/dist/lib/node/types/index.cjs +4 -4
- package/dist/lib/node/types/index.cjs.map +1 -1
- package/dist/lib/node-esm/ExplorerContainer-APGUQI4M.mjs +38 -0
- package/dist/lib/node-esm/ExplorerContainer-APGUQI4M.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-MUHCE377.mjs → chunk-34X2VFQN.mjs} +415 -154
- package/dist/lib/{browser/chunk-JL7ZGOFE.mjs.map → node-esm/chunk-34X2VFQN.mjs.map} +4 -4
- package/dist/lib/node-esm/{chunk-PUFSCMN4.mjs → chunk-3CMBLK6W.mjs} +3 -3
- package/dist/lib/node-esm/{chunk-PUFSCMN4.mjs.map → chunk-3CMBLK6W.mjs.map} +1 -1
- package/dist/lib/node-esm/{chunk-QOJSLABX.mjs → chunk-N6VEANUZ.mjs} +3 -3
- package/dist/lib/node-esm/{chunk-QOJSLABX.mjs.map → chunk-N6VEANUZ.mjs.map} +1 -1
- package/dist/lib/node-esm/chunk-PVII2K2B.mjs +179 -0
- package/dist/lib/node-esm/chunk-PVII2K2B.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-VSORIAHH.mjs +32 -0
- package/dist/lib/node-esm/chunk-VSORIAHH.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +15 -11
- package/dist/lib/node-esm/index.mjs.map +1 -1
- package/dist/lib/node-esm/{intent-resolver-XWRVHP6H.mjs → intent-resolver-4RBV644N.mjs} +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/meta.mjs +1 -1
- package/dist/lib/node-esm/{react-surface-4ZT5BQP6.mjs → react-surface-ZEVL3FXG.mjs} +5 -5
- package/dist/lib/node-esm/types/index.mjs +2 -2
- package/dist/types/src/components/ExplorerContainer.d.ts +4 -3
- package/dist/types/src/components/ExplorerContainer.d.ts.map +1 -1
- package/dist/types/src/components/Graph/D3ForceGraph.d.ts +14 -0
- package/dist/types/src/components/Graph/D3ForceGraph.d.ts.map +1 -0
- package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts +6 -0
- package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts.map +1 -0
- package/dist/types/src/components/Graph/ForceGraph.d.ts +8 -0
- package/dist/types/src/components/Graph/ForceGraph.d.ts.map +1 -0
- package/dist/types/src/components/Graph/{Graph.stories.d.ts → ForceGraph.stories.d.ts} +1 -1
- package/dist/types/src/components/Graph/ForceGraph.stories.d.ts.map +1 -0
- package/dist/types/src/components/Graph/adapter.d.ts +21 -0
- package/dist/types/src/components/Graph/adapter.d.ts.map +1 -0
- package/dist/types/src/components/Graph/index.d.ts +2 -2
- package/dist/types/src/components/Graph/index.d.ts.map +1 -1
- package/dist/types/src/components/Graph/testing.d.ts +14 -0
- package/dist/types/src/components/Graph/testing.d.ts.map +1 -0
- package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/testing/generator.d.ts +8 -0
- package/dist/types/src/components/Tree/testing/generator.d.ts.map +1 -0
- package/dist/types/src/components/Tree/testing/index.d.ts +2 -0
- package/dist/types/src/components/Tree/testing/index.d.ts.map +1 -0
- package/dist/types/src/components/Tree/types/index.d.ts +3 -0
- package/dist/types/src/components/Tree/types/index.d.ts.map +1 -0
- package/dist/types/src/components/Tree/types/tree.d.ts +83 -0
- package/dist/types/src/components/Tree/types/tree.d.ts.map +1 -0
- package/dist/types/src/components/Tree/types/tree.test.d.ts +2 -0
- package/dist/types/src/components/Tree/types/tree.test.d.ts.map +1 -0
- package/dist/types/src/components/Tree/types/types.d.ts +8 -0
- package/dist/types/src/components/Tree/types/types.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +2 -2
- package/dist/types/src/hooks/index.d.ts +2 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useGraphModel.d.ts +4 -0
- package/dist/types/src/hooks/useGraphModel.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +2 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/package.json +28 -28
- package/src/components/ExplorerContainer.tsx +11 -4
- package/src/components/Graph/D3ForceGraph.stories.tsx +64 -0
- package/src/components/Graph/D3ForceGraph.tsx +101 -0
- package/src/components/Graph/ForceGraph.stories.tsx +64 -0
- package/src/components/Graph/{Graph.tsx → ForceGraph.tsx} +19 -26
- package/src/components/Graph/adapter.ts +47 -0
- package/src/components/Graph/index.ts +2 -3
- package/src/components/Graph/testing.ts +57 -0
- package/src/components/Tree/Tree.stories.tsx +1 -1
- package/src/components/Tree/Tree.tsx +11 -18
- package/src/components/Tree/testing/generator.ts +46 -0
- package/src/components/Tree/testing/index.ts +5 -0
- package/src/components/Tree/types/index.ts +6 -0
- package/src/components/Tree/types/tree.test.ts +133 -0
- package/src/components/Tree/types/tree.ts +287 -0
- package/src/components/Tree/types/types.ts +41 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useGraphModel.ts +35 -0
- package/src/index.ts +2 -2
- package/src/meta.ts +2 -2
- package/dist/lib/browser/ExplorerContainer-6U4GS62Q.mjs +0 -27
- package/dist/lib/browser/ExplorerContainer-6U4GS62Q.mjs.map +0 -7
- package/dist/lib/browser/chunk-QRPUL5AH.mjs +0 -206
- package/dist/lib/browser/chunk-QRPUL5AH.mjs.map +0 -7
- package/dist/lib/node/ExplorerContainer-4RMWCWRX.cjs.map +0 -7
- package/dist/lib/node/chunk-4ZUNNUQD.cjs +0 -237
- package/dist/lib/node/chunk-4ZUNNUQD.cjs.map +0 -7
- package/dist/lib/node-esm/ExplorerContainer-3GOCCL7Q.mjs +0 -28
- package/dist/lib/node-esm/ExplorerContainer-3GOCCL7Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-BJZBPCC3.mjs +0 -208
- package/dist/lib/node-esm/chunk-BJZBPCC3.mjs.map +0 -7
- package/dist/types/src/components/Graph/Graph.d.ts +0 -8
- package/dist/types/src/components/Graph/Graph.d.ts.map +0 -1
- package/dist/types/src/components/Graph/Graph.stories.d.ts.map +0 -1
- package/dist/types/src/components/Graph/graph-model.d.ts +0 -39
- package/dist/types/src/components/Graph/graph-model.d.ts.map +0 -1
- package/dist/types/src/components/Tree/types.d.ts +0 -8
- package/dist/types/src/components/Tree/types.d.ts.map +0 -1
- package/src/components/Graph/Graph.stories.tsx +0 -62
- package/src/components/Graph/graph-model.ts +0 -194
- package/src/components/Tree/types.ts +0 -40
- /package/dist/lib/browser/{intent-resolver-7JF4DU2C.mjs.map → intent-resolver-FJDVBDE3.mjs.map} +0 -0
- /package/dist/lib/browser/{react-surface-JLE6GLG6.mjs.map → react-surface-H3YDMXAQ.mjs.map} +0 -0
- /package/dist/lib/node/{intent-resolver-VDAHQEE7.cjs.map → intent-resolver-DRT67ZU4.cjs.map} +0 -0
- /package/dist/lib/node/{react-surface-F66QYWDR.cjs.map → react-surface-6ESLSM33.cjs.map} +0 -0
- /package/dist/lib/node-esm/{intent-resolver-XWRVHP6H.mjs.map → intent-resolver-4RBV644N.mjs.map} +0 -0
- /package/dist/lib/node-esm/{react-surface-4ZT5BQP6.mjs.map → react-surface-ZEVL3FXG.mjs.map} +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type FC, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import { getTypename } from '@dxos/echo-schema';
|
|
8
|
+
import { SelectionModel } from '@dxos/graph';
|
|
9
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
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 { getHashColor } from '@dxos/react-ui-theme';
|
|
19
|
+
import { type SpaceGraphNode, type SpaceGraphModel, type SpaceGraphEdge } from '@dxos/schema';
|
|
20
|
+
|
|
21
|
+
import '@dxos/react-ui-graph/styles/graph.css';
|
|
22
|
+
|
|
23
|
+
export type D3ForceGraphProps = ThemedClassName<
|
|
24
|
+
{
|
|
25
|
+
model?: SpaceGraphModel;
|
|
26
|
+
match?: RegExp;
|
|
27
|
+
selection?: SelectionModel;
|
|
28
|
+
grid?: boolean;
|
|
29
|
+
} & Pick<GraphProps, 'drag'>
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
export const D3ForceGraph: FC<D3ForceGraphProps> = ({ classNames, model, selection: _selection, grid, ...props }) => {
|
|
33
|
+
const context = useRef<SVGContext>(null);
|
|
34
|
+
const projector = useMemo<GraphForceProjector | undefined>(() => {
|
|
35
|
+
if (context.current) {
|
|
36
|
+
return new GraphForceProjector(context.current, {
|
|
37
|
+
attributes: {
|
|
38
|
+
linkForce: (edge) => {
|
|
39
|
+
// TODO(burdon): Check type (currently assumes Employee property).
|
|
40
|
+
// Edge shouldn't contribute to force if it's not active.
|
|
41
|
+
return edge.data?.object?.active !== false;
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
forces: {
|
|
45
|
+
point: {
|
|
46
|
+
strength: 0.01,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}, [context.current]);
|
|
52
|
+
|
|
53
|
+
const graph = useRef<GraphController>(null);
|
|
54
|
+
const selection = useMemo(() => _selection ?? new SelectionModel(), [_selection]);
|
|
55
|
+
useEffect(() => graph.current?.repaint(), [selection.selected.value]);
|
|
56
|
+
|
|
57
|
+
const handleSelect = useCallback<NonNullable<GraphProps['onSelect']>>(
|
|
58
|
+
(node) => {
|
|
59
|
+
if (selection.contains(node.id)) {
|
|
60
|
+
selection.remove(node.id);
|
|
61
|
+
} else {
|
|
62
|
+
selection.add(node.id);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
[selection],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<SVG.Root ref={context} classNames={classNames}>
|
|
70
|
+
<SVG.Markers />
|
|
71
|
+
{grid && <SVG.Grid axis />}
|
|
72
|
+
<SVG.Zoom extent={[1 / 2, 2]}>
|
|
73
|
+
<SVG.Graph<SpaceGraphNode, SpaceGraphEdge>
|
|
74
|
+
{...props}
|
|
75
|
+
ref={graph}
|
|
76
|
+
model={model}
|
|
77
|
+
projector={projector}
|
|
78
|
+
labels={{
|
|
79
|
+
text: (node) => {
|
|
80
|
+
return node.data?.data.label ?? node.id;
|
|
81
|
+
},
|
|
82
|
+
}}
|
|
83
|
+
attributes={{
|
|
84
|
+
node: (node: GraphLayoutNode<SpaceGraphNode>) => {
|
|
85
|
+
const obj = node.data?.data.object;
|
|
86
|
+
return {
|
|
87
|
+
data: {
|
|
88
|
+
color: getHashColor(obj && getTypename(obj))?.color,
|
|
89
|
+
},
|
|
90
|
+
classes: {
|
|
91
|
+
'dx-selected': selection.contains(node.id),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
}}
|
|
96
|
+
onSelect={handleSelect}
|
|
97
|
+
/>
|
|
98
|
+
</SVG.Zoom>
|
|
99
|
+
</SVG.Root>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxos-theme';
|
|
6
|
+
|
|
7
|
+
import { type Meta } from '@storybook/react';
|
|
8
|
+
import React, { useEffect, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import { faker } from '@dxos/random';
|
|
11
|
+
import { useClient } from '@dxos/react-client';
|
|
12
|
+
import { live } from '@dxos/react-client/echo';
|
|
13
|
+
import { type Space } from '@dxos/react-client/echo';
|
|
14
|
+
import { withClientProvider } from '@dxos/react-client/testing';
|
|
15
|
+
import { DataType } from '@dxos/schema';
|
|
16
|
+
import { type ValueGenerator } from '@dxos/schema/testing';
|
|
17
|
+
import { withLayout, withTheme, render } from '@dxos/storybook-utils';
|
|
18
|
+
|
|
19
|
+
import { ForceGraph } from './ForceGraph';
|
|
20
|
+
import { generate } from './testing';
|
|
21
|
+
import { useGraphModel } from '../../hooks';
|
|
22
|
+
import { ViewType } from '../../types';
|
|
23
|
+
|
|
24
|
+
const generator = faker as any as ValueGenerator;
|
|
25
|
+
|
|
26
|
+
faker.seed(1);
|
|
27
|
+
|
|
28
|
+
const DefaultStory = () => {
|
|
29
|
+
const client = useClient();
|
|
30
|
+
const [space, setSpace] = useState<Space>();
|
|
31
|
+
const [view, setView] = useState<ViewType>();
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const space = client.spaces.default;
|
|
34
|
+
void generate(space, generator);
|
|
35
|
+
const view = space.db.add(live(ViewType, { name: '', type: '' }));
|
|
36
|
+
setSpace(space);
|
|
37
|
+
setView(view);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const model = useGraphModel(space);
|
|
41
|
+
if (!model || !space || !view) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return <ForceGraph model={model} />;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const meta: Meta = {
|
|
49
|
+
title: 'plugins/plugin-explorer/ForceGraph',
|
|
50
|
+
component: ForceGraph,
|
|
51
|
+
render: render(DefaultStory),
|
|
52
|
+
decorators: [
|
|
53
|
+
withClientProvider({
|
|
54
|
+
createSpace: true,
|
|
55
|
+
types: [ViewType, DataType.HasRelationship, DataType.Organization, DataType.Project, DataType.Person],
|
|
56
|
+
}),
|
|
57
|
+
withTheme,
|
|
58
|
+
withLayout({ fullscreen: true }),
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default meta;
|
|
63
|
+
|
|
64
|
+
export const Default = {};
|
|
@@ -3,30 +3,24 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { forceLink, forceManyBody } from 'd3';
|
|
6
|
-
import
|
|
6
|
+
import NativeForceGraph from 'force-graph';
|
|
7
7
|
import React, { type FC, useEffect, useRef } from 'react';
|
|
8
8
|
import { useResizeDetector } from 'react-resize-detector';
|
|
9
9
|
|
|
10
|
-
import { type Space } from '@dxos/client/echo';
|
|
11
10
|
import { filterObjectsSync, type SearchResult } from '@dxos/plugin-search';
|
|
12
|
-
import {
|
|
11
|
+
import { type SpaceGraphModel } from '@dxos/schema';
|
|
13
12
|
|
|
14
|
-
import {
|
|
13
|
+
import { GraphAdapter } from './adapter';
|
|
15
14
|
|
|
16
|
-
export type
|
|
17
|
-
|
|
15
|
+
export type ForceGraphProps = {
|
|
16
|
+
model?: SpaceGraphModel;
|
|
18
17
|
match?: RegExp;
|
|
19
18
|
};
|
|
20
19
|
|
|
21
|
-
export const
|
|
20
|
+
export const ForceGraph: FC<ForceGraphProps> = ({ model, match }) => {
|
|
22
21
|
const { ref, width, height } = useResizeDetector({ refreshRate: 200 });
|
|
23
22
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
24
|
-
const forceGraph = useRef<
|
|
25
|
-
|
|
26
|
-
const [model] = useAsyncState(
|
|
27
|
-
async () => (space ? new SpaceGraphModel({ schema: true }).open(space) : undefined),
|
|
28
|
-
[space],
|
|
29
|
-
);
|
|
23
|
+
const forceGraph = useRef<NativeForceGraph>();
|
|
30
24
|
|
|
31
25
|
const filteredRef = useRef<SearchResult[]>();
|
|
32
26
|
filteredRef.current = filterObjectsSync(model?.objects ?? [], match);
|
|
@@ -35,17 +29,14 @@ export const Graph: FC<GraphProps> = ({ space, match }) => {
|
|
|
35
29
|
if (rootRef.current) {
|
|
36
30
|
// https://github.com/vasturiano/force-graph
|
|
37
31
|
// https://github.com/vasturiano/3d-force-graph
|
|
38
|
-
forceGraph.current = new
|
|
32
|
+
forceGraph.current = new NativeForceGraph(rootRef.current)
|
|
33
|
+
// https://github.com/vasturiano/force-graph?tab=readme-ov-file#node-styling
|
|
39
34
|
.nodeRelSize(6)
|
|
40
|
-
.nodeLabel((node: any) =>
|
|
41
|
-
if (node.type === 'schema') {
|
|
42
|
-
return node.data.typename;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return node.id;
|
|
46
|
-
})
|
|
35
|
+
.nodeLabel((node: any) => (node.type === 'schema' ? node.data.typename : node.data.label ?? node.id))
|
|
47
36
|
.nodeAutoColorBy((node: any) => (node.type === 'schema' ? 'schema' : node.data.typename))
|
|
48
|
-
|
|
37
|
+
|
|
38
|
+
// https://github.com/vasturiano/force-graph?tab=readme-ov-file#link-styling
|
|
39
|
+
.linkAutoColorBy((link: any) => link.type);
|
|
49
40
|
}
|
|
50
41
|
|
|
51
42
|
return () => {
|
|
@@ -56,6 +47,7 @@ export const Graph: FC<GraphProps> = ({ space, match }) => {
|
|
|
56
47
|
|
|
57
48
|
useEffect(() => {
|
|
58
49
|
if (forceGraph.current && width && height && model) {
|
|
50
|
+
// https://github.com/vasturiano/force-graph?tab=readme-ov-file#container-layout
|
|
59
51
|
forceGraph.current
|
|
60
52
|
.pauseAnimation()
|
|
61
53
|
.width(width)
|
|
@@ -63,17 +55,18 @@ export const Graph: FC<GraphProps> = ({ space, match }) => {
|
|
|
63
55
|
.onEngineStop(() => {
|
|
64
56
|
handleZoomToFit();
|
|
65
57
|
})
|
|
58
|
+
.onNodeClick((node: any) => {
|
|
59
|
+
forceGraph.current?.emitParticle(node);
|
|
60
|
+
})
|
|
66
61
|
|
|
67
62
|
// https://github.com/vasturiano/force-graph?tab=readme-ov-file#force-engine-d3-force-configuration
|
|
68
63
|
// .d3Force('center', forceCenter().strength(0.9))
|
|
69
64
|
.d3Force('link', forceLink().distance(160).strength(0.5))
|
|
70
65
|
.d3Force('charge', forceManyBody().strength(-30))
|
|
71
|
-
// .d3AlphaDecay(0.0228)
|
|
72
|
-
// .d3VelocityDecay(0.4)
|
|
73
66
|
|
|
74
|
-
.graphData(model
|
|
67
|
+
.graphData(new GraphAdapter(model))
|
|
75
68
|
.warmupTicks(100)
|
|
76
|
-
.cooldownTime(
|
|
69
|
+
.cooldownTime(1_000)
|
|
77
70
|
.resumeAnimation();
|
|
78
71
|
}
|
|
79
72
|
}, [model, width, height]);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Graph } from '@dxos/graph';
|
|
6
|
+
|
|
7
|
+
export type GraphNode = {
|
|
8
|
+
id?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type GraphLink = {
|
|
12
|
+
source?: string;
|
|
13
|
+
target?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type GraphData = {
|
|
17
|
+
nodes: GraphNode[];
|
|
18
|
+
links: GraphLink[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class GraphAdapter implements GraphData {
|
|
22
|
+
private readonly _nodes: GraphNode[] = [];
|
|
23
|
+
private readonly _links: GraphLink[] = [];
|
|
24
|
+
|
|
25
|
+
constructor(private readonly graph: Graph) {
|
|
26
|
+
this._nodes = graph.nodes.map((node) => ({
|
|
27
|
+
id: node.id,
|
|
28
|
+
type: node.type,
|
|
29
|
+
data: node.data,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
this._links = graph.edges.map((edge) => ({
|
|
33
|
+
type: edge.type,
|
|
34
|
+
source: edge.source,
|
|
35
|
+
target: edge.target,
|
|
36
|
+
data: edge.data,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get nodes() {
|
|
41
|
+
return this._nodes;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get links() {
|
|
45
|
+
return this._links;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type AnyLiveObject, live, type Space } from '@dxos/client/echo';
|
|
6
|
+
import { Query, RelationSourceId, RelationTargetId } from '@dxos/echo-schema';
|
|
7
|
+
import { DataType } from '@dxos/schema';
|
|
8
|
+
import { createObjectFactory, type ValueGenerator, type TypeSpec } from '@dxos/schema/testing';
|
|
9
|
+
import { range } from '@dxos/util';
|
|
10
|
+
|
|
11
|
+
const getObject = (objects: AnyLiveObject[]) => objects[Math.floor(Math.random() * objects.length)];
|
|
12
|
+
|
|
13
|
+
const defaultTypes: TypeSpec[] = [
|
|
14
|
+
{ type: DataType.Organization, count: 5 },
|
|
15
|
+
{ type: DataType.Project, count: 5 },
|
|
16
|
+
{ type: DataType.Person, count: 10 },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export type GenerateOptions = {
|
|
20
|
+
spec?: TypeSpec[];
|
|
21
|
+
relations?: {
|
|
22
|
+
count: number;
|
|
23
|
+
kind: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const defaultRelations: GenerateOptions['relations'] = { count: 10, kind: 'friend' };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated Use @dxos/schema.
|
|
31
|
+
*/
|
|
32
|
+
export const generate = async (
|
|
33
|
+
space: Space,
|
|
34
|
+
generator: ValueGenerator,
|
|
35
|
+
{ spec = defaultTypes, relations = defaultRelations }: GenerateOptions = {},
|
|
36
|
+
) => {
|
|
37
|
+
const createObjects = createObjectFactory(space.db, generator);
|
|
38
|
+
await createObjects(spec);
|
|
39
|
+
|
|
40
|
+
// Add relations between objects.
|
|
41
|
+
const { objects: contacts } = await space.db.query(Query.type(DataType.Person)).run();
|
|
42
|
+
for (const _ of range(relations.count)) {
|
|
43
|
+
const source = getObject(contacts);
|
|
44
|
+
const target = getObject(contacts);
|
|
45
|
+
if (source.id === target.id) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
space.db.add(
|
|
50
|
+
live(DataType.HasRelationship, {
|
|
51
|
+
kind: relations.kind,
|
|
52
|
+
[RelationSourceId]: source,
|
|
53
|
+
[RelationTargetId]: target,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -7,13 +7,13 @@ import '@dxos-theme';
|
|
|
7
7
|
import { type Meta } from '@storybook/react';
|
|
8
8
|
import React, { type FC, useEffect, useState } from 'react';
|
|
9
9
|
|
|
10
|
-
import { TreeType, Tree as TreeModel } from '@dxos/plugin-outliner/types';
|
|
11
10
|
import { faker } from '@dxos/random';
|
|
12
11
|
import { useClient } from '@dxos/react-client';
|
|
13
12
|
import { type ClientRepeatedComponentProps, ClientRepeater } from '@dxos/react-client/testing';
|
|
14
13
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
15
14
|
|
|
16
15
|
import { Tree, type TreeComponentProps } from './Tree';
|
|
16
|
+
import { TreeType, Tree as TreeModel } from './types';
|
|
17
17
|
|
|
18
18
|
// TODO(burdon): Storybook for Graph/Tree/Plot (generics); incl. GraphModel.
|
|
19
19
|
// TODO(burdon): Type for all Explorer components (Space, Object, Query, etc.) incl.
|
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, { useEffect, useState } from 'react';
|
|
6
|
-
import { useResizeDetector } from 'react-resize-detector';
|
|
5
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
7
6
|
|
|
8
7
|
import { type Space } from '@dxos/client/echo';
|
|
9
|
-
import { createSvgContext, SVG, SVGRoot } from '@dxos/gem-core';
|
|
10
8
|
import { useAsyncState } from '@dxos/react-ui';
|
|
9
|
+
import { SVG, type SVGContext } from '@dxos/react-ui-graph';
|
|
10
|
+
import { SpaceGraphModel } from '@dxos/schema';
|
|
11
11
|
|
|
12
12
|
import { HierarchicalEdgeBundling, RadialTree, TidyTree } from './layout';
|
|
13
13
|
import { mapGraphToTreeData, type TreeNode } from './types';
|
|
14
|
-
import { SpaceGraphModel } from '../Graph';
|
|
15
14
|
|
|
16
15
|
// TODO(burdon): Create dge bundling graph using d3.hierarchy.
|
|
17
16
|
// https://observablehq.com/@d3/hierarchical-edge-bundling?intent=fork
|
|
@@ -63,10 +62,7 @@ export type TreeComponentProps<N = unknown> = {
|
|
|
63
62
|
|
|
64
63
|
// TODO(burdon): Label accessor.
|
|
65
64
|
export const Tree = <N,>({ space, selected, variant = 'tidy', onNodeClick }: TreeComponentProps<N>) => {
|
|
66
|
-
const [model] = useAsyncState(
|
|
67
|
-
async () => (space ? new SpaceGraphModel().open(space, selected) : undefined),
|
|
68
|
-
[space, selected],
|
|
69
|
-
);
|
|
65
|
+
const [model] = useAsyncState(async () => (space ? new SpaceGraphModel().open(space) : undefined), [space, selected]);
|
|
70
66
|
|
|
71
67
|
const [tree, setTree] = useState<TreeNode>();
|
|
72
68
|
useEffect(() => {
|
|
@@ -76,11 +72,11 @@ export const Tree = <N,>({ space, selected, variant = 'tidy', onNodeClick }: Tre
|
|
|
76
72
|
}, true);
|
|
77
73
|
}, [model]);
|
|
78
74
|
|
|
79
|
-
const context =
|
|
80
|
-
const { ref, width = 0, height = 0 } = useResizeDetector();
|
|
75
|
+
const context = useRef<SVGContext>(null);
|
|
81
76
|
|
|
82
77
|
useEffect(() => {
|
|
83
|
-
if (
|
|
78
|
+
if (context.current) {
|
|
79
|
+
const { width, height } = context.current.size!;
|
|
84
80
|
const size = Math.min(width, height);
|
|
85
81
|
const radius = size * 0.4;
|
|
86
82
|
const options = {
|
|
@@ -98,17 +94,14 @@ export const Tree = <N,>({ space, selected, variant = 'tidy', onNodeClick }: Tre
|
|
|
98
94
|
|
|
99
95
|
if (tree) {
|
|
100
96
|
const renderer = renderers.get(variant);
|
|
101
|
-
renderer?.(context.
|
|
97
|
+
renderer?.(context.current!.svg, tree, options);
|
|
102
98
|
}
|
|
103
99
|
}
|
|
104
|
-
}, [
|
|
100
|
+
}, [context.current, tree]);
|
|
105
101
|
|
|
106
|
-
// TODO(burdon): Provider should expand.
|
|
107
102
|
return (
|
|
108
|
-
<div
|
|
109
|
-
<
|
|
110
|
-
<SVG />
|
|
111
|
-
</SVGRoot>
|
|
103
|
+
<div onClick={() => onNodeClick?.()}>
|
|
104
|
+
<SVG.Root ref={context} />
|
|
112
105
|
</div>
|
|
113
106
|
);
|
|
114
107
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { ObjectId } from '@dxos/echo-schema';
|
|
6
|
+
import { range } from '@dxos/util';
|
|
7
|
+
|
|
8
|
+
import { Tree, type TreeNodeType } from '../types';
|
|
9
|
+
|
|
10
|
+
type NumberOrNumberArray = number | number[];
|
|
11
|
+
|
|
12
|
+
const random = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create hierarchical tree.
|
|
16
|
+
*/
|
|
17
|
+
export const createTree = (spec: NumberOrNumberArray[] = [], createText?: () => string): Tree => {
|
|
18
|
+
const tree = new Tree();
|
|
19
|
+
tree.root.data = { text: 'root' };
|
|
20
|
+
|
|
21
|
+
const createNodes = (parent: TreeNodeType, spec: NumberOrNumberArray = 0): TreeNodeType[] => {
|
|
22
|
+
const count = Array.isArray(spec) ? random(spec[0], spec[1]) : spec;
|
|
23
|
+
return range(count, (i) => ({
|
|
24
|
+
id: ObjectId.random(),
|
|
25
|
+
children: [],
|
|
26
|
+
data: {
|
|
27
|
+
text: createText?.() ?? [parent.data.text, i + 1].join('.'),
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const createChildNodes = (parent: TreeNodeType, [count = 0, ...rest]: NumberOrNumberArray[]): TreeNodeType => {
|
|
33
|
+
const nodes = createNodes(parent, count);
|
|
34
|
+
nodes.forEach((n) => tree.addNode(parent, n));
|
|
35
|
+
if (rest.length) {
|
|
36
|
+
for (const node of nodes) {
|
|
37
|
+
createChildNodes(node, rest);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return parent;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
createChildNodes(tree.root, spec);
|
|
45
|
+
return tree;
|
|
46
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { live, makeRef } from '@dxos/live-object';
|
|
8
|
+
import { faker } from '@dxos/random';
|
|
9
|
+
import { DataType } from '@dxos/schema';
|
|
10
|
+
|
|
11
|
+
import { type Tree } from './tree';
|
|
12
|
+
import { createTree } from '../testing';
|
|
13
|
+
|
|
14
|
+
faker.seed(0);
|
|
15
|
+
|
|
16
|
+
const print = (tree: Tree) => {
|
|
17
|
+
let count = 0;
|
|
18
|
+
tree.tranverse((node, i) => {
|
|
19
|
+
console.log(''.padStart(i * 2, ' '), node.data);
|
|
20
|
+
count++;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return count;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('tree', () => {
|
|
27
|
+
test('tree', ({ expect }) => {
|
|
28
|
+
{
|
|
29
|
+
const tree = createTree();
|
|
30
|
+
let count = 0;
|
|
31
|
+
tree.tranverse(() => {
|
|
32
|
+
count++;
|
|
33
|
+
});
|
|
34
|
+
expect(count).to.eq(tree.size);
|
|
35
|
+
expect(count).to.eq(1);
|
|
36
|
+
}
|
|
37
|
+
{
|
|
38
|
+
const tree = createTree([10]);
|
|
39
|
+
let count = 0;
|
|
40
|
+
tree.tranverse(() => {
|
|
41
|
+
count++;
|
|
42
|
+
});
|
|
43
|
+
expect(count).to.eq(tree.size);
|
|
44
|
+
expect(count).to.eq(1 + 10);
|
|
45
|
+
}
|
|
46
|
+
{
|
|
47
|
+
const tree = createTree([10, 3, 1]);
|
|
48
|
+
let count = 0;
|
|
49
|
+
tree.tranverse(() => {
|
|
50
|
+
count++;
|
|
51
|
+
});
|
|
52
|
+
expect(count).to.eq(tree.size);
|
|
53
|
+
expect(count).to.eq(1 + 10 * (1 + 3 * (1 + 1))); // 71
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('tree navigation', ({ expect }) => {
|
|
58
|
+
const tree = createTree([2, 3, 1]);
|
|
59
|
+
expect(tree.getParent(tree.root)).to.be.null;
|
|
60
|
+
|
|
61
|
+
const nodes = tree.getChildNodes(tree.root);
|
|
62
|
+
expect(nodes).to.have.length(2);
|
|
63
|
+
|
|
64
|
+
const first = nodes[0];
|
|
65
|
+
expect(first.children).to.have.length(3);
|
|
66
|
+
const parent = tree.getParent(first);
|
|
67
|
+
expect(parent).to.eq(tree.root);
|
|
68
|
+
|
|
69
|
+
const [c1, c2, c3] = tree.getChildNodes(first);
|
|
70
|
+
expect(tree.getParent(c1)).to.eq(first);
|
|
71
|
+
expect(tree.getParent(c2)).to.eq(first);
|
|
72
|
+
expect(tree.getParent(c3)).to.eq(first);
|
|
73
|
+
|
|
74
|
+
const [g1] = tree.getChildNodes(c1);
|
|
75
|
+
expect(tree.getParent(g1)).to.eq(c1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* root root root
|
|
80
|
+
* └── 1 └── 1 └── 1
|
|
81
|
+
* ├── 1.1 ├── 1.1 ├── 1.1
|
|
82
|
+
* ├── 1.2 <- indent │ ├── 1.2 <- unindent ├── 1.2
|
|
83
|
+
* ├── 1.3 <- indent │ └── 1.3 │ └── 1.3
|
|
84
|
+
* ├── 1.4 ├── 1.4 ├── 1.4
|
|
85
|
+
* └── 1.5 └── 1.5 └── 1.5
|
|
86
|
+
*/
|
|
87
|
+
test('indent and unindent', async ({ expect }) => {
|
|
88
|
+
const tree = createTree([1, 5]);
|
|
89
|
+
const parent = tree.getNode(tree.root.children[0]);
|
|
90
|
+
|
|
91
|
+
{
|
|
92
|
+
const count = print(tree);
|
|
93
|
+
expect(count).to.eq(1 + 1 * (1 + 5));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Indent
|
|
97
|
+
{
|
|
98
|
+
const child = tree.getChildNodes(parent);
|
|
99
|
+
tree.indentNode(child[1]);
|
|
100
|
+
tree.indentNode(child[2]);
|
|
101
|
+
expect(parent.children).to.have.length(3);
|
|
102
|
+
expect(child[0].children).to.have.length(2);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
const count = print(tree);
|
|
107
|
+
expect(count).to.eq(1 + 1 * (1 + 5));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Unindent
|
|
111
|
+
{
|
|
112
|
+
const child = tree.getChildNodes(parent);
|
|
113
|
+
const grandchild = tree.getChildNodes(child[0]);
|
|
114
|
+
tree.unindentNode(grandchild[0]);
|
|
115
|
+
expect(grandchild[0].children).to.have.length(1);
|
|
116
|
+
expect(parent.children).to.have.length(4);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
{
|
|
120
|
+
const count = print(tree);
|
|
121
|
+
expect(count).to.eq(1 + 1 * (1 + 5));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('task', ({ expect }) => {
|
|
126
|
+
const task = live(DataType.Task, { text: 'Test task.' });
|
|
127
|
+
expect(task.text).to.eq('Test task.');
|
|
128
|
+
|
|
129
|
+
const tree = createTree();
|
|
130
|
+
const node = tree.addNode(tree.root);
|
|
131
|
+
node.ref = makeRef(task);
|
|
132
|
+
});
|
|
133
|
+
});
|