@dxos/plugin-explorer 0.8.4-main.abd8ff62ef → 0.8.4-main.bc2380dfbc
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/LICENSE +102 -5
- package/dist/lib/neutral/ExplorerArticle-EW2MBCRK.mjs +141 -0
- package/dist/lib/neutral/ExplorerArticle-EW2MBCRK.mjs.map +7 -0
- package/dist/lib/neutral/ExplorerPlugin.mjs +10 -0
- package/dist/lib/neutral/capabilities/index.mjs +11 -0
- package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
- package/dist/lib/{browser/types/index.mjs → neutral/chunk-7SPMPHRS.mjs} +7 -7
- package/dist/lib/neutral/chunk-7SPMPHRS.mjs.map +7 -0
- package/dist/lib/neutral/chunk-GRJXLL4Z.mjs +25 -0
- package/dist/lib/neutral/chunk-GRJXLL4Z.mjs.map +7 -0
- package/dist/lib/{browser → neutral}/components/index.mjs +661 -297
- package/dist/lib/{browser → neutral}/components/index.mjs.map +4 -4
- package/dist/lib/neutral/containers/index.mjs +9 -0
- package/dist/lib/neutral/containers/index.mjs.map +7 -0
- package/dist/lib/neutral/create-object-F6TKVAGV.mjs +39 -0
- package/dist/lib/neutral/create-object-F6TKVAGV.mjs.map +7 -0
- package/dist/lib/{browser → neutral}/hooks/index.mjs +11 -6
- package/dist/lib/neutral/hooks/index.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +14 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/{browser/index.mjs → neutral/plugin.mjs} +3 -4
- package/dist/lib/{browser/index.mjs.map → neutral/plugin.mjs.map} +3 -3
- package/dist/lib/neutral/react-surface-APBW2VQG.mjs +26 -0
- package/dist/lib/neutral/react-surface-APBW2VQG.mjs.map +7 -0
- package/dist/lib/neutral/testing.mjs +8 -0
- package/dist/lib/neutral/testing.mjs.map +7 -0
- package/dist/lib/neutral/translations.mjs +33 -0
- package/dist/lib/neutral/translations.mjs.map +7 -0
- package/dist/lib/neutral/types/index.mjs +10 -0
- package/dist/lib/neutral/types/index.mjs.map +7 -0
- package/dist/types/src/ExplorerPlugin.d.ts.map +1 -1
- package/dist/types/src/capabilities/create-object.d.ts +11 -0
- package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +6 -0
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/components/Chart/Chart.stories.d.ts +4 -1
- package/dist/types/src/components/Chart/Chart.stories.d.ts.map +1 -1
- package/dist/types/src/components/Globe/Globe.stories.d.ts +5 -2
- package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
- package/dist/types/src/components/Graph/CanvasForceGraph.d.ts +13 -0
- package/dist/types/src/components/Graph/CanvasForceGraph.d.ts.map +1 -0
- package/dist/types/src/components/Graph/{D3ForceGraph.stories.d.ts → CanvasForceGraph.stories.d.ts} +2 -2
- package/dist/types/src/components/Graph/CanvasForceGraph.stories.d.ts.map +1 -0
- package/dist/types/src/components/Graph/ForceGraph.d.ts +12 -5
- package/dist/types/src/components/Graph/ForceGraph.d.ts.map +1 -1
- package/dist/types/src/components/Graph/ForceGraph.stories.d.ts +3 -1
- package/dist/types/src/components/Graph/ForceGraph.stories.d.ts.map +1 -1
- package/dist/types/src/components/Graph/{adapter.d.ts → graph-adapter.d.ts} +1 -1
- package/dist/types/src/components/Graph/graph-adapter.d.ts.map +1 -0
- package/dist/types/src/components/Graph/index.d.ts +1 -1
- package/dist/types/src/components/Graph/index.d.ts.map +1 -1
- package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts +21 -0
- package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts.map +1 -0
- package/dist/types/src/components/Tree/Tree.d.ts +20 -23
- package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.stories.d.ts +5 -12
- package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
- package/dist/types/src/components/Tree/index.d.ts +3 -0
- package/dist/types/src/components/Tree/index.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts +35 -2
- package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/RadialTree.d.ts +35 -2
- package/dist/types/src/components/Tree/layout/RadialTree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/TidyTree.d.ts +24 -2
- package/dist/types/src/components/Tree/layout/TidyTree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/hierarchy.d.ts +17 -0
- package/dist/types/src/components/Tree/layout/hierarchy.d.ts.map +1 -0
- package/dist/types/src/components/Tree/layout/index.d.ts +5 -4
- package/dist/types/src/components/Tree/layout/index.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/slots.d.ts +7 -0
- package/dist/types/src/components/Tree/layout/slots.d.ts.map +1 -0
- package/dist/types/src/components/Tree/layout/useContainerSize.d.ts +15 -0
- package/dist/types/src/components/Tree/layout/useContainerSize.d.ts.map +1 -0
- package/dist/types/src/components/Tree/space-graph-adapter.d.ts +32 -0
- package/dist/types/src/components/Tree/space-graph-adapter.d.ts.map +1 -0
- package/dist/types/src/components/Tree/testing/index.d.ts +1 -0
- package/dist/types/src/components/Tree/testing/index.d.ts.map +1 -1
- package/dist/types/src/components/Tree/testing/relations.d.ts +47 -0
- package/dist/types/src/components/Tree/testing/relations.d.ts.map +1 -0
- package/dist/types/src/components/Tree/types/types.d.ts +14 -4
- package/dist/types/src/components/Tree/types/types.d.ts.map +1 -1
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts +8 -0
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts.map +1 -0
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts +24 -0
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/ExplorerArticle/index.d.ts +2 -0
- package/dist/types/src/containers/ExplorerArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/index.d.ts +1 -1
- package/dist/types/src/containers/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useGraphModel.d.ts +2 -2
- package/dist/types/src/hooks/useGraphModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/plugin.d.ts +3 -0
- package/dist/types/src/plugin.d.ts.map +1 -0
- package/dist/types/src/testing.d.ts +2 -0
- package/dist/types/src/testing.d.ts.map +1 -0
- package/dist/types/src/types/Graph.d.ts +1 -2
- package/dist/types/src/types/Graph.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +94 -67
- package/src/ExplorerPlugin.test.ts +2 -2
- package/src/ExplorerPlugin.tsx +3 -33
- package/src/capabilities/create-object.ts +36 -0
- package/src/capabilities/index.ts +1 -0
- package/src/capabilities/react-surface.tsx +2 -2
- package/src/components/Chart/Chart.stories.tsx +14 -20
- package/src/components/Globe/Globe.stories.tsx +17 -19
- package/src/components/Graph/CanvasForceGraph.stories.tsx +83 -0
- package/src/components/Graph/CanvasForceGraph.tsx +124 -0
- package/src/components/Graph/ForceGraph.stories.tsx +69 -37
- package/src/components/Graph/ForceGraph.tsx +104 -85
- package/src/components/Graph/index.ts +1 -1
- package/src/components/Tree/EdgeBundling.stories.tsx +144 -0
- package/src/components/Tree/Tree.stories.tsx +17 -38
- package/src/components/Tree/Tree.tsx +69 -100
- package/src/components/Tree/index.ts +3 -0
- package/src/components/Tree/layout/HierarchicalEdgeBundling.tsx +277 -0
- package/src/components/Tree/layout/RadialTree.tsx +237 -0
- package/src/components/Tree/layout/TidyTree.tsx +246 -0
- package/src/components/Tree/layout/hierarchy.ts +32 -0
- package/src/components/Tree/layout/index.ts +5 -5
- package/src/components/Tree/layout/slots.ts +19 -0
- package/src/components/Tree/layout/useContainerSize.ts +43 -0
- package/src/components/Tree/space-graph-adapter.ts +96 -0
- package/src/components/Tree/testing/index.ts +1 -0
- package/src/components/Tree/testing/relations.ts +182 -0
- package/src/components/Tree/types/types.ts +38 -29
- package/src/containers/ExplorerArticle/ExplorerArticle.stories.tsx +119 -0
- package/src/containers/ExplorerArticle/ExplorerArticle.tsx +153 -0
- package/src/containers/ExplorerArticle/index.ts +5 -0
- package/src/containers/index.ts +1 -1
- package/src/hooks/useGraphModel.ts +10 -6
- package/src/index.ts +1 -6
- package/src/plugin.ts +9 -0
- package/src/testing.ts +7 -0
- package/src/types/ExplorerAction.ts +1 -1
- package/src/types/Graph.ts +1 -2
- package/dist/lib/browser/hooks/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/types/index.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-6EUBRHHX.mjs +0 -26
- package/dist/lib/node-esm/chunk-6EUBRHHX.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/components/index.mjs +0 -11255
- package/dist/lib/node-esm/components/index.mjs.map +0 -7
- package/dist/lib/node-esm/hooks/index.mjs +0 -41
- package/dist/lib/node-esm/hooks/index.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -14
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/meta.mjs +0 -9
- package/dist/lib/node-esm/types/index.mjs +0 -73
- package/dist/lib/node-esm/types/index.mjs.map +0 -7
- package/dist/types/src/components/Graph/D3ForceGraph.d.ts +0 -15
- package/dist/types/src/components/Graph/D3ForceGraph.d.ts.map +0 -1
- package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts.map +0 -1
- package/dist/types/src/components/Graph/adapter.d.ts.map +0 -1
- package/dist/types/src/components/Graph/testing.d.ts +0 -14
- package/dist/types/src/components/Graph/testing.d.ts.map +0 -1
- package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts +0 -6
- package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts.map +0 -1
- package/dist/types/src/containers/ExplorerContainer/index.d.ts +0 -2
- package/dist/types/src/containers/ExplorerContainer/index.d.ts.map +0 -1
- package/src/components/Graph/D3ForceGraph.stories.tsx +0 -83
- package/src/components/Graph/D3ForceGraph.tsx +0 -108
- package/src/components/Graph/testing.ts +0 -58
- package/src/components/Tree/layout/HierarchicalEdgeBundling.ts +0 -162
- package/src/components/Tree/layout/RadialTree.ts +0 -94
- package/src/components/Tree/layout/TidyTree.ts +0 -101
- package/src/containers/ExplorerContainer/ExplorerContainer.tsx +0 -53
- package/src/containers/ExplorerContainer/index.ts +0 -5
- /package/dist/lib/{browser/chunk-J5LGTIGS.mjs.map → neutral/ExplorerPlugin.mjs.map} +0 -0
- /package/dist/lib/{browser → neutral}/chunk-HPIS2WXY.mjs +0 -0
- /package/dist/lib/{browser → neutral}/chunk-HPIS2WXY.mjs.map +0 -0
- /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs +0 -0
- /package/dist/lib/{browser/meta.mjs.map → neutral/chunk-J5LGTIGS.mjs.map} +0 -0
- /package/dist/lib/{node-esm/chunk-HSLMI22Q.mjs.map → neutral/index.mjs.map} +0 -0
- /package/dist/lib/{browser → neutral}/meta.mjs +0 -0
- /package/dist/lib/{node-esm → neutral}/meta.mjs.map +0 -0
- /package/src/components/Graph/{adapter.ts → graph-adapter.ts} +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Space } from '@dxos/client/echo';
|
|
6
|
+
import { Obj, Query, Relation } from '@dxos/echo';
|
|
7
|
+
import { type TypeSpec, type ValueGenerator, createObjectFactory } from '@dxos/schema/testing';
|
|
8
|
+
import { HasConnection, HasRelationship, Organization, Person, Pipeline } from '@dxos/types';
|
|
9
|
+
import { range } from '@dxos/util';
|
|
10
|
+
|
|
11
|
+
import { type BundleEdge } from '../layout';
|
|
12
|
+
import { type TreeNode } from '../types';
|
|
13
|
+
|
|
14
|
+
const SECTORS = ['Technology', 'Finance', 'Research', 'Media'];
|
|
15
|
+
const CONNECTION_KINDS = ['partner', 'investor', 'vendor', 'customer'];
|
|
16
|
+
|
|
17
|
+
const pick = <T>(arr: readonly T[], rng = Math.random): T => arr[Math.floor(rng() * arr.length)];
|
|
18
|
+
|
|
19
|
+
export type ConnectedOrgsResult = {
|
|
20
|
+
organizations: Obj.Any[];
|
|
21
|
+
people: Obj.Any[];
|
|
22
|
+
connections: Obj.Any[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ConnectedOrgsOptions = {
|
|
26
|
+
organizationCount?: number;
|
|
27
|
+
personCount?: number;
|
|
28
|
+
connectionCount?: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Populate a space with Organizations, People, and HasConnection relations between organizations.
|
|
33
|
+
* Uses `createObjectFactory` to generate Org/Person properties from their `GeneratorAnnotation`s,
|
|
34
|
+
* then layers manual HasConnection relations on top — the connection schema is fixed
|
|
35
|
+
* (Org→Org) so it isn't a fit for the generator's reference inference.
|
|
36
|
+
*/
|
|
37
|
+
export const generateConnectedOrgs = async (
|
|
38
|
+
space: Space,
|
|
39
|
+
generator: ValueGenerator,
|
|
40
|
+
{ organizationCount = 12, personCount = 24, connectionCount = 18 }: ConnectedOrgsOptions = {},
|
|
41
|
+
): Promise<ConnectedOrgsResult> => {
|
|
42
|
+
const specs: TypeSpec[] = [
|
|
43
|
+
{ type: Organization.Organization, count: organizationCount },
|
|
44
|
+
// Person has a Ref to Organization — generator fills it from objects already in db.
|
|
45
|
+
{ type: Person.Person, count: personCount },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const factory = createObjectFactory(space.db, generator);
|
|
49
|
+
await factory(specs);
|
|
50
|
+
|
|
51
|
+
const organizations = await space.db.query(Query.type(Organization.Organization)).run();
|
|
52
|
+
const people = await space.db.query(Query.type(Person.Person)).run();
|
|
53
|
+
|
|
54
|
+
const connections: Obj.Any[] = [];
|
|
55
|
+
const seen = new Set<string>();
|
|
56
|
+
for (let i = 0; i < connectionCount && organizations.length >= 2; i++) {
|
|
57
|
+
const source = pick(organizations);
|
|
58
|
+
const target = pick(organizations);
|
|
59
|
+
if (source.id === target.id) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const key = `${source.id}->${target.id}`;
|
|
63
|
+
if (seen.has(key)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
seen.add(key);
|
|
67
|
+
|
|
68
|
+
const relation = Relation.make(HasConnection.HasConnection, {
|
|
69
|
+
[Relation.Source]: source as any,
|
|
70
|
+
[Relation.Target]: target as any,
|
|
71
|
+
kind: pick(CONNECTION_KINDS),
|
|
72
|
+
});
|
|
73
|
+
connections.push(space.db.add(relation as any));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await space.db.flush();
|
|
77
|
+
return { organizations, people, connections };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build a TreeNode hierarchy: Root → Sector → Organization (leaf).
|
|
82
|
+
* Organizations are deterministically bucketed into `SECTORS` so the demo has visible groups.
|
|
83
|
+
*/
|
|
84
|
+
export const buildOrgHierarchy = (organizations: Obj.Any[], sectors: readonly string[] = SECTORS): TreeNode => {
|
|
85
|
+
// Avoid modulo-by-zero / missing-bucket crashes when the caller passes an empty sectors list.
|
|
86
|
+
const activeSectors = sectors.length > 0 ? sectors : ['Uncategorized'];
|
|
87
|
+
const buckets = new Map<string, TreeNode[]>();
|
|
88
|
+
for (const sector of activeSectors) {
|
|
89
|
+
buckets.set(sector, []);
|
|
90
|
+
}
|
|
91
|
+
for (let i = 0; i < organizations.length; i++) {
|
|
92
|
+
const org = organizations[i] as any;
|
|
93
|
+
const sector = activeSectors[i % activeSectors.length];
|
|
94
|
+
buckets.get(sector)!.push({
|
|
95
|
+
id: org.id,
|
|
96
|
+
label: org.name ?? org.id.slice(0, 6),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
id: 'root',
|
|
102
|
+
label: 'Organizations',
|
|
103
|
+
children: activeSectors.map((sector) => ({
|
|
104
|
+
id: `sector:${sector}`,
|
|
105
|
+
label: sector,
|
|
106
|
+
children: buckets.get(sector) ?? [],
|
|
107
|
+
})),
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const defaultGenerateTypes: TypeSpec[] = [
|
|
112
|
+
{ type: Organization.Organization, count: 20 },
|
|
113
|
+
{ type: Person.Person, count: 20 },
|
|
114
|
+
{ type: Pipeline.Pipeline, count: 20 },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
export type GenerateOptions = {
|
|
118
|
+
spec?: TypeSpec[];
|
|
119
|
+
relations?: {
|
|
120
|
+
count: number;
|
|
121
|
+
kind: string;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const defaultGenerateRelations: NonNullable<GenerateOptions['relations']> = {
|
|
126
|
+
kind: 'friend',
|
|
127
|
+
count: 10,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Populate a space with a mixed dataset (Orgs, Pipelines, People) plus
|
|
132
|
+
* `HasRelationship` edges between random pairs of People.
|
|
133
|
+
*
|
|
134
|
+
* Used by the force-directed and canvas-force graph stories that want a
|
|
135
|
+
* heterogeneous typed dataset without caring about the precise shape of relations.
|
|
136
|
+
*/
|
|
137
|
+
export const generate = async (
|
|
138
|
+
space: Space,
|
|
139
|
+
generator: ValueGenerator,
|
|
140
|
+
{ spec = defaultGenerateTypes, relations = defaultGenerateRelations }: GenerateOptions = {},
|
|
141
|
+
) => {
|
|
142
|
+
const createObjects = createObjectFactory(space.db, generator);
|
|
143
|
+
await createObjects(spec);
|
|
144
|
+
|
|
145
|
+
const contacts: Obj.Any[] = await space.db.query(Query.type(Person.Person)).run();
|
|
146
|
+
if (contacts.length < 2 || relations.count <= 0) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
for (const _ of range(relations.count)) {
|
|
150
|
+
const source = pick(contacts);
|
|
151
|
+
const target = pick(contacts);
|
|
152
|
+
if (source.id !== target.id) {
|
|
153
|
+
space.db.add(
|
|
154
|
+
Relation.make(HasRelationship.HasRelationship, {
|
|
155
|
+
[Relation.Source]: source as any,
|
|
156
|
+
[Relation.Target]: target as any,
|
|
157
|
+
kind: relations.kind,
|
|
158
|
+
}) as any,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Convert HasConnection relations into bundle edges between organization ids.
|
|
166
|
+
*/
|
|
167
|
+
export const connectionsToEdges = (connections: Obj.Any[]): BundleEdge[] => {
|
|
168
|
+
return connections
|
|
169
|
+
.map((relation): BundleEdge | undefined => {
|
|
170
|
+
const source = Relation.getSource(relation as any) as any;
|
|
171
|
+
const target = Relation.getTarget(relation as any) as any;
|
|
172
|
+
if (!source?.id || !target?.id) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
source: source.id,
|
|
177
|
+
target: target.id,
|
|
178
|
+
kind: (relation as any).kind,
|
|
179
|
+
};
|
|
180
|
+
})
|
|
181
|
+
.filter((e): e is BundleEdge => Boolean(e));
|
|
182
|
+
};
|
|
@@ -2,40 +2,49 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type
|
|
5
|
+
import { type Key } from '@dxos/echo';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import { type TreeType } from './tree';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* In-memory tree shape used by the d3 layouts.
|
|
11
|
+
* `data` carries through to layout callbacks (e.g. hover/inspect) — typically an ECHO object on leaves.
|
|
12
|
+
*/
|
|
13
|
+
export type TreeNode<TData = unknown> = {
|
|
8
14
|
id: string;
|
|
9
15
|
label?: string;
|
|
10
|
-
|
|
16
|
+
data?: TData;
|
|
17
|
+
children?: TreeNode<TData>[];
|
|
11
18
|
};
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Convert an ECHO `TreeType` (id-keyed node map) into a nested `TreeNode` hierarchy.
|
|
22
|
+
* Returns `undefined` if the root id is missing — the tree is then incomplete and shouldn't render.
|
|
23
|
+
*/
|
|
24
|
+
export const treeTypeToTreeNode = (
|
|
25
|
+
tree: TreeType,
|
|
26
|
+
rootId: Key.ObjectId = tree.root,
|
|
27
|
+
visited: Set<string> = new Set(),
|
|
28
|
+
): TreeNode | undefined => {
|
|
29
|
+
const node = tree.nodes[rootId];
|
|
30
|
+
if (!node) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (visited.has(rootId)) {
|
|
34
|
+
return { id: rootId, label: labelOf(node), data: node.data };
|
|
35
|
+
}
|
|
36
|
+
visited.add(rootId);
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// }
|
|
38
|
-
// }
|
|
38
|
+
return {
|
|
39
|
+
id: rootId,
|
|
40
|
+
label: labelOf(node),
|
|
41
|
+
data: node.data,
|
|
42
|
+
children: node.children
|
|
43
|
+
.map((childId) => treeTypeToTreeNode(tree, childId, visited))
|
|
44
|
+
.filter((c): c is TreeNode => Boolean(c)),
|
|
45
|
+
};
|
|
46
|
+
};
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
const labelOf = (node: { data: Record<string, any> }): string | undefined => {
|
|
49
|
+
return typeof node.data?.text === 'string' ? node.data.text : undefined;
|
|
41
50
|
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
import { withPluginManager } from '@dxos/app-framework/testing';
|
|
10
|
+
import { Filter, Query, Type, View } from '@dxos/echo';
|
|
11
|
+
import { ClientPlugin, initializeIdentity } from '@dxos/plugin-client/testing';
|
|
12
|
+
import { PreviewPlugin } from '@dxos/plugin-preview/testing';
|
|
13
|
+
import { StorybookPlugin, 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
|
+
import { ViewModel } from '@dxos/schema';
|
|
18
|
+
import { type ValueGenerator } from '@dxos/schema/testing';
|
|
19
|
+
import { HasRelationship, Organization, Person, Pipeline } from '@dxos/types';
|
|
20
|
+
|
|
21
|
+
import { generate } from '../../components/Tree/testing';
|
|
22
|
+
import { Graph } from '../../types';
|
|
23
|
+
import { ExplorerArticle, type ExplorerArticleVariant } from './ExplorerArticle';
|
|
24
|
+
|
|
25
|
+
const generator = random as any as ValueGenerator;
|
|
26
|
+
|
|
27
|
+
random.seed(7);
|
|
28
|
+
|
|
29
|
+
type StoryArgs = { variant: ExplorerArticleVariant };
|
|
30
|
+
|
|
31
|
+
const DefaultStory = ({ variant }: StoryArgs) => {
|
|
32
|
+
const [space] = useSpaces();
|
|
33
|
+
const [graph] = useQuery(space?.db, Filter.type(Graph.Graph));
|
|
34
|
+
if (!space || !graph) {
|
|
35
|
+
return <Loading data={{ space: !!space, graph: !!graph }} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return <ExplorerArticle role='article' subject={graph as any} attendableId={graph.id} variant={variant} />;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const meta: Meta<StoryArgs> = {
|
|
42
|
+
title: 'plugins/plugin-explorer/containers/ExplorerArticle',
|
|
43
|
+
render: DefaultStory,
|
|
44
|
+
decorators: [
|
|
45
|
+
withTheme(),
|
|
46
|
+
withLayout({ layout: 'fullscreen' }),
|
|
47
|
+
withPluginManager({
|
|
48
|
+
plugins: [
|
|
49
|
+
...corePlugins(),
|
|
50
|
+
StorybookPlugin({}),
|
|
51
|
+
ClientPlugin({
|
|
52
|
+
types: [
|
|
53
|
+
Graph.Graph,
|
|
54
|
+
View.View,
|
|
55
|
+
HasRelationship.HasRelationship,
|
|
56
|
+
Organization.Organization,
|
|
57
|
+
Pipeline.Pipeline,
|
|
58
|
+
Person.Person,
|
|
59
|
+
],
|
|
60
|
+
onClientInitialized: ({ client }) =>
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
const { personalSpace } = yield* initializeIdentity(client);
|
|
63
|
+
yield* Effect.promise(() => generate(personalSpace, generator));
|
|
64
|
+
const { view } = yield* Effect.promise(() =>
|
|
65
|
+
ViewModel.makeFromDatabase({ db: personalSpace.db, typename: Type.getTypename(Graph.Graph) }),
|
|
66
|
+
);
|
|
67
|
+
const graph = personalSpace.db.add(
|
|
68
|
+
Graph.make({
|
|
69
|
+
name: 'Test',
|
|
70
|
+
view,
|
|
71
|
+
query: { raw: '', ast: Query.select(Filter.everything()).ast },
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
yield* Effect.promise(() => personalSpace.db.flush({ indexes: true }));
|
|
75
|
+
return graph;
|
|
76
|
+
}),
|
|
77
|
+
}),
|
|
78
|
+
PreviewPlugin(),
|
|
79
|
+
],
|
|
80
|
+
}),
|
|
81
|
+
],
|
|
82
|
+
parameters: {
|
|
83
|
+
layout: 'fullscreen',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default meta;
|
|
88
|
+
|
|
89
|
+
type Story = StoryObj<StoryArgs>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Default force-directed view (the production layout).
|
|
93
|
+
*/
|
|
94
|
+
export const Force: Story = {
|
|
95
|
+
args: {
|
|
96
|
+
variant: 'force',
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Radial cluster: every object on the perimeter, grouped by its schema, all under
|
|
102
|
+
* a single database root. Inspired by https://observablehq.com/@d3/radial-cluster.
|
|
103
|
+
*/
|
|
104
|
+
export const Cluster: Story = {
|
|
105
|
+
args: {
|
|
106
|
+
variant: 'cluster',
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Hierarchical edge bundling: same hierarchy as `cluster`, with bundled curves
|
|
112
|
+
* routed through the lowest common ancestor for every relation / ref in the space.
|
|
113
|
+
* Inspired by https://observablehq.com/@d3/hierarchical-edge-bundling.
|
|
114
|
+
*/
|
|
115
|
+
export const Bundle: Story = {
|
|
116
|
+
args: {
|
|
117
|
+
variant: 'bundle',
|
|
118
|
+
},
|
|
119
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { type AppSurface } from '@dxos/app-toolkit/ui';
|
|
9
|
+
import { type Filter, Obj, type View } from '@dxos/echo';
|
|
10
|
+
import { QueryBuilder } from '@dxos/echo-query';
|
|
11
|
+
import { useObject } from '@dxos/react-client/echo';
|
|
12
|
+
import { DxAnchorActivate, Icon, Panel, Toolbar } from '@dxos/react-ui';
|
|
13
|
+
import { QueryEditor, type QueryEditorProps } from '@dxos/react-ui-components';
|
|
14
|
+
|
|
15
|
+
import { ForceGraph } from '#components';
|
|
16
|
+
import { HierarchicalEdgeBundling, RadialTree, spaceGraphToHierarchy, type TreeNode } from '#components';
|
|
17
|
+
import { useGraphModel } from '#hooks';
|
|
18
|
+
|
|
19
|
+
/** Visualization variants exposed by `ExplorerArticle`. */
|
|
20
|
+
export type ExplorerArticleVariant = 'force' | 'cluster' | 'bundle';
|
|
21
|
+
|
|
22
|
+
const VARIANTS: { value: ExplorerArticleVariant; icon: string; label: string }[] = [
|
|
23
|
+
{
|
|
24
|
+
value: 'force',
|
|
25
|
+
icon: 'ph--graph--regular',
|
|
26
|
+
label: 'Force-directed',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
value: 'cluster',
|
|
30
|
+
icon: 'ph--asterisk-simple--regular',
|
|
31
|
+
label: 'Radial cluster',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
value: 'bundle',
|
|
35
|
+
icon: 'ph--circles-three-plus--regular',
|
|
36
|
+
label: 'Edge bundling',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export type ExplorerArticleProps = AppSurface.ObjectArticleProps<View.View>;
|
|
41
|
+
|
|
42
|
+
export const ExplorerArticle = ({ role, subject, variant }: ExplorerArticleProps) => {
|
|
43
|
+
const [view] = useObject(subject);
|
|
44
|
+
const db = view && Obj.getDatabase(view);
|
|
45
|
+
const [filter, setFilter] = useState<Filter.Any>();
|
|
46
|
+
const model = useGraphModel(db, filter);
|
|
47
|
+
|
|
48
|
+
const builder = useMemo(() => new QueryBuilder(), []);
|
|
49
|
+
const handleChange = useCallback<NonNullable<QueryEditorProps['onChange']>>((value) => {
|
|
50
|
+
setFilter(builder.build(value).filter);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
// The `variant` prop is the initial value; user can toggle via the toolbar tabs.
|
|
54
|
+
const [selected, setSelected] = useState<ExplorerArticleVariant>(isVariant(variant) ? variant : 'force');
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (isVariant(variant)) {
|
|
57
|
+
setSelected(variant);
|
|
58
|
+
}
|
|
59
|
+
}, [variant]);
|
|
60
|
+
const handleVariantChange = useCallback((value: string) => {
|
|
61
|
+
if (isVariant(value)) {
|
|
62
|
+
setSelected(value);
|
|
63
|
+
}
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const handleHoverPreview = useCallback((node: TreeNode | null, event?: MouseEvent) => {
|
|
67
|
+
if (!node || !event) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const obj = node.data;
|
|
71
|
+
if (!obj || !Obj.isObject(obj)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const dxn = Obj.getDXN(obj)?.toString();
|
|
75
|
+
if (!dxn) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const target = event.target as HTMLElement;
|
|
79
|
+
target.dispatchEvent(
|
|
80
|
+
new DxAnchorActivate({
|
|
81
|
+
dxn,
|
|
82
|
+
label: Obj.getLabel(obj) ?? dxn,
|
|
83
|
+
trigger: target,
|
|
84
|
+
kind: 'card',
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
const showToolbar = role === 'article';
|
|
90
|
+
|
|
91
|
+
if (!db || !model) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Panel.Root role={role}>
|
|
97
|
+
{showToolbar && (
|
|
98
|
+
<Panel.Toolbar asChild>
|
|
99
|
+
<Toolbar.Root>
|
|
100
|
+
<QueryEditor db={db} onChange={handleChange} />
|
|
101
|
+
<Toolbar.ToggleGroup type='single' value={selected} onValueChange={handleVariantChange}>
|
|
102
|
+
{VARIANTS.map(({ value, icon, label }) => (
|
|
103
|
+
<Toolbar.ToggleGroupItem key={value} value={value} aria-label={label} title={label}>
|
|
104
|
+
<Icon icon={icon} size={4} />
|
|
105
|
+
</Toolbar.ToggleGroupItem>
|
|
106
|
+
))}
|
|
107
|
+
</Toolbar.ToggleGroup>
|
|
108
|
+
</Toolbar.Root>
|
|
109
|
+
</Panel.Toolbar>
|
|
110
|
+
)}
|
|
111
|
+
<Panel.Content>
|
|
112
|
+
<Visualization variant={selected} model={model} onNodeHover={handleHoverPreview} />
|
|
113
|
+
</Panel.Content>
|
|
114
|
+
</Panel.Root>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const isVariant = (value: unknown): value is ExplorerArticleVariant =>
|
|
119
|
+
value === 'force' || value === 'cluster' || value === 'bundle';
|
|
120
|
+
|
|
121
|
+
type VisualizationProps = {
|
|
122
|
+
variant: ExplorerArticleVariant;
|
|
123
|
+
model: NonNullable<ReturnType<typeof useGraphModel>>;
|
|
124
|
+
onNodeHover?: (node: TreeNode | null, event?: MouseEvent) => void;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const Visualization = ({ variant, model, onNodeHover }: VisualizationProps) => {
|
|
128
|
+
if (variant === 'force') {
|
|
129
|
+
// ForceGraph subscribes to model.graphAtom internally; don't re-render the wrapper on every tick.
|
|
130
|
+
return (
|
|
131
|
+
<ForceGraph
|
|
132
|
+
model={model}
|
|
133
|
+
onInspect={(node, event) => onNodeHover?.({ id: node.id, data: node.data?.data?.object }, event)}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return <HierarchyVisualization variant={variant} model={model} onNodeHover={onNodeHover} />;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Read from the model's reactive graph atom so the hierarchy is rebuilt as objects/relations stream in.
|
|
143
|
+
*/
|
|
144
|
+
const HierarchyVisualization = ({ variant, model, onNodeHover }: VisualizationProps) => {
|
|
145
|
+
// Capture the atom snapshot so the memo's dep list explicitly tracks each push from the atom.
|
|
146
|
+
const graphSnapshot = useAtomValue(model.graphAtom);
|
|
147
|
+
const { tree, edges } = useMemo(() => spaceGraphToHierarchy(model), [model, graphSnapshot]);
|
|
148
|
+
if (variant === 'cluster') {
|
|
149
|
+
return <RadialTree data={tree} cluster onNodeHover={onNodeHover} />;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return <HierarchicalEdgeBundling data={tree} edges={edges} onNodeHover={onNodeHover} />;
|
|
153
|
+
};
|
package/src/containers/index.ts
CHANGED
|
@@ -6,38 +6,42 @@ import { useEffect, useState } from 'react';
|
|
|
6
6
|
|
|
7
7
|
import { Capabilities } from '@dxos/app-framework';
|
|
8
8
|
import { useCapability } from '@dxos/app-framework/ui';
|
|
9
|
-
import { type
|
|
9
|
+
import { type Database, type Entity, type Filter } from '@dxos/echo';
|
|
10
10
|
import { SpaceGraphModel, type SpaceGraphModelOptions } from '@dxos/schema';
|
|
11
11
|
|
|
12
12
|
// TODO(burdon): Factor out.
|
|
13
13
|
export const useGraphModel = (
|
|
14
|
-
|
|
14
|
+
db: Database.Database | undefined,
|
|
15
15
|
filter?: Filter.Any | undefined,
|
|
16
16
|
options?: SpaceGraphModelOptions,
|
|
17
|
-
|
|
17
|
+
items?: readonly Entity.Unknown[],
|
|
18
18
|
): SpaceGraphModel | undefined => {
|
|
19
19
|
const registry = useCapability(Capabilities.AtomRegistry);
|
|
20
20
|
const [model, setModel] = useState<SpaceGraphModel | undefined>(undefined);
|
|
21
21
|
|
|
22
22
|
useEffect(() => {
|
|
23
|
-
if (!
|
|
23
|
+
if (!db) {
|
|
24
24
|
setModel(undefined);
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const newModel = new SpaceGraphModel(registry);
|
|
29
|
-
void newModel.open(
|
|
29
|
+
void newModel.open(db);
|
|
30
30
|
setModel(newModel);
|
|
31
31
|
|
|
32
32
|
return () => {
|
|
33
33
|
setModel(undefined);
|
|
34
34
|
void newModel.close();
|
|
35
35
|
};
|
|
36
|
-
}, [
|
|
36
|
+
}, [db, registry]);
|
|
37
37
|
|
|
38
38
|
useEffect(() => {
|
|
39
39
|
model?.setFilter(filter).setOptions(options);
|
|
40
40
|
}, [model, filter, options]);
|
|
41
41
|
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
model?.setItems(items);
|
|
44
|
+
}, [model, items]);
|
|
45
|
+
|
|
42
46
|
return model;
|
|
43
47
|
};
|
package/src/index.ts
CHANGED
package/src/plugin.ts
ADDED
package/src/testing.ts
ADDED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import * as Schema from 'effect/Schema';
|
|
6
6
|
|
|
7
|
-
import { TypeInputOptionsAnnotation } from '@dxos/plugin-space
|
|
7
|
+
import { TypeInputOptionsAnnotation } from '@dxos/plugin-space';
|
|
8
8
|
|
|
9
9
|
export const GraphProps = Schema.Struct({
|
|
10
10
|
name: Schema.optional(Schema.String),
|
package/src/types/Graph.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import * as Schema from 'effect/Schema';
|
|
6
6
|
|
|
7
|
-
import { Annotation, Filter, Obj, Query, QueryAST, Ref, Type } from '@dxos/echo';
|
|
8
|
-
import { View } from '@dxos/echo';
|
|
7
|
+
import { Annotation, Filter, Obj, Query, QueryAST, Ref, Type, View } from '@dxos/echo';
|
|
9
8
|
import { FormInputAnnotation, LabelAnnotation } from '@dxos/echo/internal';
|
|
10
9
|
import { ViewAnnotation } from '@dxos/schema';
|
|
11
10
|
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../../src/hooks/useGraphModel.ts"],
|
|
4
|
-
"sourcesContent": ["//\n// Copyright 2023 DXOS.org\n//\n\nimport { useEffect, useState } from 'react';\n\nimport { Capabilities } from '@dxos/app-framework';\nimport { useCapability } from '@dxos/app-framework/ui';\nimport { type Filter, type Queue, type Space } from '@dxos/client/echo';\nimport { SpaceGraphModel, type SpaceGraphModelOptions } from '@dxos/schema';\n\n// TODO(burdon): Factor out.\nexport const useGraphModel = (\n space: Space | undefined,\n filter?: Filter.Any | undefined,\n options?: SpaceGraphModelOptions,\n queue?: Queue,\n): SpaceGraphModel | undefined => {\n const registry = useCapability(Capabilities.AtomRegistry);\n const [model, setModel] = useState<SpaceGraphModel | undefined>(undefined);\n\n useEffect(() => {\n if (!space) {\n setModel(undefined);\n return;\n }\n\n const newModel = new SpaceGraphModel(registry);\n void newModel.open(space.db, queue);\n setModel(newModel);\n\n return () => {\n setModel(undefined);\n void newModel.close();\n };\n }, [space, registry, queue]);\n\n useEffect(() => {\n model?.setFilter(filter).setOptions(options);\n }, [model, filter, options]);\n\n return model;\n};\n"],
|
|
5
|
-
"mappings": ";;;AAIA,SAASA,WAAWC,gBAAgB;AAEpC,SAASC,oBAAoB;AAC7B,SAASC,qBAAqB;AAE9B,SAASC,uBAAoD;AAGtD,IAAMC,gBAAgB,CAC3BC,OACAC,QACAC,SACAC,UAAAA;AAEA,QAAMC,WAAWP,cAAcD,aAAaS,YAAY;AACxD,QAAM,CAACC,OAAOC,QAAAA,IAAYZ,SAAsCa,MAAAA;AAEhEd,YAAU,MAAA;AACR,QAAI,CAACM,OAAO;AACVO,eAASC,MAAAA;AACT;IACF;AAEA,UAAMC,WAAW,IAAIX,gBAAgBM,QAAAA;AACrC,SAAKK,SAASC,KAAKV,MAAMW,IAAIR,KAAAA;AAC7BI,aAASE,QAAAA;AAET,WAAO,MAAA;AACLF,eAASC,MAAAA;AACT,WAAKC,SAASG,MAAK;IACrB;EACF,GAAG;IAACZ;IAAOI;IAAUD;GAAM;AAE3BT,YAAU,MAAA;AACRY,WAAOO,UAAUZ,MAAAA,EAAQa,WAAWZ,OAAAA;EACtC,GAAG;IAACI;IAAOL;IAAQC;GAAQ;AAE3B,SAAOI;AACT;",
|
|
6
|
-
"names": ["useEffect", "useState", "Capabilities", "useCapability", "SpaceGraphModel", "useGraphModel", "space", "filter", "options", "queue", "registry", "AtomRegistry", "model", "setModel", "undefined", "newModel", "open", "db", "close", "setFilter", "setOptions"]
|
|
7
|
-
}
|