From df3da17143a782a14379e37fd4fed4ab87b77c6a Mon Sep 17 00:00:00 2001 From: Ji Kim <111468570+jikim-msft@users.noreply.github.com> Date: Thu, 16 Jan 2025 00:31:12 +0000 Subject: [PATCH 1/8] include allowedTypes for root field --- .../data-visualization/DefaultVisualizers.ts | 32 +- .../SharedTreeVisualizer.ts | 2 +- .../src/test/DefaultVisualizers.spec.ts | 374 ++++++++++++++++++ .../devtools-test-app/src/FluidObject.ts | 49 ++- 4 files changed, 430 insertions(+), 27 deletions(-) diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts index 77400415cf12..e3b2e9de13b5 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts @@ -22,16 +22,21 @@ import { SharedMatrix } from "@fluidframework/matrix/internal"; import { SharedString } from "@fluidframework/sequence/internal"; import type { ISharedObject } from "@fluidframework/shared-object-base/internal"; import type { ITreeInternal } from "@fluidframework/tree/internal"; -import { SharedTree } from "@fluidframework/tree/internal"; +import { SchemaFactory, SharedTree, Tree } from "@fluidframework/tree/internal"; import { EditType } from "../CommonInterfaces.js"; import type { VisualizeChildData, VisualizeSharedObject } from "./DataVisualization.js"; import { + concatenateTypes, determineNodeKind, toVisualTree, visualizeSharedTreeNodeBySchema, } from "./SharedTreeVisualizer.js"; +import { + VisualSharedTreeNodeKind, + type VisualSharedTreeNode, +} from "./VisualSharedTreeTypes.js"; import { type FluidObjectNode, type FluidObjectTreeNode, @@ -260,8 +265,6 @@ export const visualizeSharedTree: VisualizeSharedObject = async ( // Root node of the SharedTree's content. const treeView = sharedTree.exportVerbose(); - // TODO: this visualizer doesn't consider the root as a field, and thus does not display the allowed types or handle when it is empty. - // Tracked by https://dev.azure.com/fluidframework/internal/_workitems/edit/26472. if (treeView === undefined) { throw new Error("Support for visualizing empty trees is not implemented"); } @@ -269,17 +272,26 @@ export const visualizeSharedTree: VisualizeSharedObject = async ( // Schema of the tree node. const treeSchema = sharedTree.exportSimpleSchema(); - // Traverses the SharedTree and generates a visual representation of the tree and its schema. - const visualTreeRepresentation = await visualizeSharedTreeNodeBySchema( - treeView, - treeSchema, - visualizeChildData, - ); + const sf = new SchemaFactory(undefined); + const schemaName = Tree.is(treeView, [sf.boolean, sf.null, sf.number, sf.handle, sf.string]) + ? Tree.schema(treeView).identifier + : treeView.type; + + // Create a root field visualization that shows the allowed types at the root + const visualTreeRepresentation: VisualSharedTreeNode = { + schema: { + schemaName, + allowedTypes: concatenateTypes(treeSchema.allowedTypes), + }, + fields: { + root: await visualizeSharedTreeNodeBySchema(treeView, treeSchema, visualizeChildData), + }, + kind: VisualSharedTreeNodeKind.InternalNode, + }; // Maps the `visualTreeRepresentation` in the format compatible to {@link visualizeChildData} function. const visualTree = toVisualTree(visualTreeRepresentation); - // TODO: Validate the type casting. const visualTreeResult: FluidObjectNode = { ...visualTree, fluidObjectId: sharedTree.id, diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts index ba0723df9d7b..45961ffe9443 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts @@ -150,7 +150,7 @@ export function toVisualTree(tree: VisualSharedTreeNode): VisualChildNode { /** * Concatenrate allowed types for `ObjectNodeStoredSchema` and `MapNodeStoredSchema`. */ -function concatenateTypes(fieldTypes: ReadonlySet): string { +export function concatenateTypes(fieldTypes: ReadonlySet): string { return [...fieldTypes].join(" | "); } diff --git a/packages/tools/devtools/devtools-core/src/test/DefaultVisualizers.spec.ts b/packages/tools/devtools/devtools-core/src/test/DefaultVisualizers.spec.ts index bd922daf590e..0e9575438591 100644 --- a/packages/tools/devtools/devtools-core/src/test/DefaultVisualizers.spec.ts +++ b/packages/tools/devtools/devtools-core/src/test/DefaultVisualizers.spec.ts @@ -1730,6 +1730,380 @@ describe("DefaultVisualizers unit tests", () => { expect(result).to.deep.equal(expected); }); + it.only("SharedTree: Renders multiple allowed types in SharedTree's root field", async () => { + const factory = SharedTree.getFactory(); + const builder = new SchemaFactory("shared-tree-test"); + + const sharedTree = factory.create( + new MockFluidDataStoreRuntime({ idCompressor: createIdCompressor() }), + "test", + ); + + const view = sharedTree.viewWith( + new TreeViewConfiguration({ schema: [builder.string, builder.number] }), + ); + view.initialize(23); + + const result = await visualizeSharedTree( + sharedTree as unknown as ISharedObject, + visualizeChildData, + ); + + const expected = { + children: { + root: { + value: 23, + nodeKind: "ValueNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "com.fluidframework.leaf.number", + }, + }, + }, + }, + }, + }, + nodeKind: "FluidTreeNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "root", + }, + allowedTypes: { + value: "com.fluidframework.leaf.string | com.fluidframework.leaf.number", + nodeKind: "ValueNode", + }, + }, + }, + }, + fluidObjectId: "test", + typeMetadata: "SharedTree", + }; + + expect(result).to.deep.equal(expected); + }); + + it.only("SharedTree: Renders multiple allowed types in SharedTree's root field", async () => { + const factory = SharedTree.getFactory(); + const builder = new SchemaFactory("shared-tree-test"); + + const sharedTree = factory.create( + new MockFluidDataStoreRuntime({ idCompressor: createIdCompressor() }), + "test", + ); + + class LeafSchema extends builder.object("leaf-item", { + leafField: [builder.boolean, builder.handle, builder.string], + }) {} + + class ChildSchema extends builder.object("child-item", { + childField: [builder.string, builder.boolean], + childData: builder.optional(LeafSchema), + }) {} + + class RootNodeTwoItemTwo extends builder.object("root-node-two-item-two", { + childrenOne: builder.array(ChildSchema), + childrenTwo: builder.number, + }) {} + + class RootNodeTwoItem extends builder.object("root-node-item", { + childrenOne: builder.number, + childrenTwo: RootNodeTwoItemTwo, + }) {} + + class RootNodeOne extends builder.object("root-node-one", { + leafField: [builder.boolean, builder.handle, builder.string], + }) {} + + class RootNodeTwo extends builder.object("root-node-two", { + childField: RootNodeTwoItem, + }) {} + + const view = sharedTree.viewWith( + new TreeViewConfiguration({ + schema: [RootNodeOne, RootNodeTwo, builder.string, builder.number], + }), + ); + + view.initialize({ + childField: { + childrenOne: 42, + childrenTwo: { + childrenOne: [ + { + childField: false, + childData: { + leafField: "leaf data", + }, + }, + { + childField: true, + }, + ], + childrenTwo: 123, + }, + }, + }); + + const result = await visualizeSharedTree( + sharedTree as unknown as ISharedObject, + visualizeChildData, + ); + + const expected = { + children: { + root: { + children: { + childField: { + children: { + childrenOne: { + value: 42, + nodeKind: "ValueNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "com.fluidframework.leaf.number", + }, + }, + }, + }, + }, + childrenTwo: { + children: { + childrenOne: { + children: { + "0": { + children: { + childField: { + value: false, + nodeKind: "ValueNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "com.fluidframework.leaf.boolean", + }, + }, + }, + }, + }, + childData: { + children: { + leafField: { + value: "leaf data", + nodeKind: "ValueNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "com.fluidframework.leaf.string", + }, + }, + }, + }, + }, + }, + nodeKind: "TreeNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "shared-tree-test.leaf-item", + }, + allowedTypes: { + value: + "{ leafField : com.fluidframework.leaf.boolean | com.fluidframework.leaf.handle | com.fluidframework.leaf.string }", + nodeKind: "ValueNode", + }, + }, + }, + }, + }, + }, + nodeKind: "TreeNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "shared-tree-test.child-item", + }, + allowedTypes: { + value: + "{ childField : com.fluidframework.leaf.string | com.fluidframework.leaf.boolean, childData : shared-tree-test.leaf-item }", + nodeKind: "ValueNode", + }, + }, + }, + }, + }, + "1": { + children: { + childField: { + value: true, + nodeKind: "ValueNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "com.fluidframework.leaf.boolean", + }, + }, + }, + }, + }, + }, + nodeKind: "TreeNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "shared-tree-test.child-item", + }, + allowedTypes: { + value: + "{ childField : com.fluidframework.leaf.string | com.fluidframework.leaf.boolean, childData : shared-tree-test.leaf-item }", + nodeKind: "ValueNode", + }, + }, + }, + }, + }, + }, + nodeKind: "TreeNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: 'shared-tree-test.Array<["shared-tree-test.child-item"]>', + }, + allowedTypes: { + value: "shared-tree-test.child-item", + nodeKind: "ValueNode", + }, + }, + }, + }, + }, + childrenTwo: { + value: 123, + nodeKind: "ValueNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "com.fluidframework.leaf.number", + }, + }, + }, + }, + }, + }, + nodeKind: "TreeNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "shared-tree-test.root-node-two-item-two", + }, + allowedTypes: { + value: + '{ childrenOne : shared-tree-test.Array<["shared-tree-test.child-item"]>, childrenTwo : com.fluidframework.leaf.number }', + nodeKind: "ValueNode", + }, + }, + }, + }, + }, + }, + nodeKind: "TreeNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "shared-tree-test.root-node-item", + }, + allowedTypes: { + value: + "{ childrenOne : com.fluidframework.leaf.number, childrenTwo : shared-tree-test.root-node-two-item-two }", + nodeKind: "ValueNode", + }, + }, + }, + }, + }, + }, + nodeKind: "TreeNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "shared-tree-test.root-node-two", + }, + allowedTypes: { + value: "{ childField : shared-tree-test.root-node-item }", + nodeKind: "ValueNode", + }, + }, + }, + }, + }, + }, + nodeKind: "FluidTreeNode", + tooltipContents: { + schema: { + nodeKind: "TreeNode", + children: { + name: { + nodeKind: "ValueNode", + value: "shared-tree-test.root-node-two", + }, + allowedTypes: { + value: + "shared-tree-test.root-node-one | shared-tree-test.root-node-two | com.fluidframework.leaf.string | com.fluidframework.leaf.number", + nodeKind: "ValueNode", + }, + }, + }, + }, + fluidObjectId: "test", + typeMetadata: "SharedTree", + }; + + expect(result).to.deep.equal(expected); + }); + it("Unknown SharedObject", async () => { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const unknownObject = { diff --git a/packages/tools/devtools/devtools-test-app/src/FluidObject.ts b/packages/tools/devtools/devtools-test-app/src/FluidObject.ts index d498309e0a12..fdc16fa42a9c 100644 --- a/packages/tools/devtools/devtools-test-app/src/FluidObject.ts +++ b/packages/tools/devtools/devtools-test-app/src/FluidObject.ts @@ -189,29 +189,46 @@ export class AppData extends DataObject { childData: builder.optional(LeafSchema), }) {} - class RootNodeSchema extends builder.object("root-item", { + class RootNodeTwoItemTwo extends builder.object("root-node-two-item-two", { childrenOne: builder.array(ChildSchema), childrenTwo: builder.number, }) {} - const config = new TreeViewConfiguration({ schema: RootNodeSchema }); + class RootNodeTwoItem extends builder.object("root-node-item", { + childrenOne: builder.number, + childrenTwo: RootNodeTwoItemTwo, + }) {} + + class RootNodeOne extends builder.object("root-node-one", { + leafField: [builder.boolean, builder.handle, builder.string], + }) {} + + class RootNodeTwo extends builder.object("root-node-two", { + childField: RootNodeTwoItem, + }) {} + + const config = new TreeViewConfiguration({ + schema: [RootNodeOne, RootNodeTwo, builder.string, builder.number], + }); const view = sharedTree.viewWith(config); view.initialize({ - childrenOne: [ - { - childField: "Hello world!", - childData: { - leafField: "Hello world again!", - }, + childField: { + childrenOne: 42, + childrenTwo: { + childrenOne: [ + { + childField: false, + childData: { + leafField: "leaf data", + }, + }, + { + childField: true, + }, + ], + childrenTwo: 123, }, - { - childField: true, - childData: { - leafField: false, - }, - }, - ], - childrenTwo: 32, + }, }); } } From fd50e40a68c784edc4746b42dbb629bf55d3f8f4 Mon Sep 17 00:00:00 2001 From: Ji Kim <111468570+jikim-msft@users.noreply.github.com> Date: Thu, 16 Jan 2025 01:08:33 +0000 Subject: [PATCH 2/8] add condition --- .../data-visualization/DefaultVisualizers.ts | 18 ++++++++++++------ .../data-visualization/SharedTreeVisualizer.ts | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts index e3b2e9de13b5..595a9cd7aa89 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts @@ -32,6 +32,7 @@ import { determineNodeKind, toVisualTree, visualizeSharedTreeNodeBySchema, + visualizeVerboseNodeFields, } from "./SharedTreeVisualizer.js"; import { VisualSharedTreeNodeKind, @@ -273,9 +274,8 @@ export const visualizeSharedTree: VisualizeSharedObject = async ( const treeSchema = sharedTree.exportSimpleSchema(); const sf = new SchemaFactory(undefined); - const schemaName = Tree.is(treeView, [sf.boolean, sf.null, sf.number, sf.handle, sf.string]) - ? Tree.schema(treeView).identifier - : treeView.type; + const isLeaf = Tree.is(treeView, [sf.boolean, sf.null, sf.number, sf.handle, sf.string]); + const schemaName = isLeaf ? Tree.schema(treeView).identifier : treeView.type; // Create a root field visualization that shows the allowed types at the root const visualTreeRepresentation: VisualSharedTreeNode = { @@ -283,9 +283,15 @@ export const visualizeSharedTree: VisualizeSharedObject = async ( schemaName, allowedTypes: concatenateTypes(treeSchema.allowedTypes), }, - fields: { - root: await visualizeSharedTreeNodeBySchema(treeView, treeSchema, visualizeChildData), - }, + fields: isLeaf + ? { + root: await visualizeSharedTreeNodeBySchema( + treeView, + treeSchema, + visualizeChildData, + ), + } + : await visualizeVerboseNodeFields(treeView, treeSchema, visualizeChildData), kind: VisualSharedTreeNodeKind.InternalNode, }; diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts index 45961ffe9443..6c5f81ebca4e 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts @@ -171,7 +171,7 @@ function getObjectAllowedTypes(schema: SimpleObjectNodeSchema): string { /** * Returns the schema & fields of the node. */ -async function visualizeVerboseNodeFields( +export async function visualizeVerboseNodeFields( tree: VerboseTreeNode, treeSchema: SimpleTreeSchema, visualizeChildData: VisualizeChildData, From cf42f7a82f769a1c3852efcebae5e50fe572d07f Mon Sep 17 00:00:00 2001 From: Ji Kim <111468570+jikim-msft@users.noreply.github.com> Date: Thu, 16 Jan 2025 01:09:57 +0000 Subject: [PATCH 3/8] Revert "add condition" This reverts commit fd50e40a68c784edc4746b42dbb629bf55d3f8f4. --- .../data-visualization/DefaultVisualizers.ts | 18 ++++++------------ .../data-visualization/SharedTreeVisualizer.ts | 2 +- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts index 595a9cd7aa89..e3b2e9de13b5 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts @@ -32,7 +32,6 @@ import { determineNodeKind, toVisualTree, visualizeSharedTreeNodeBySchema, - visualizeVerboseNodeFields, } from "./SharedTreeVisualizer.js"; import { VisualSharedTreeNodeKind, @@ -274,8 +273,9 @@ export const visualizeSharedTree: VisualizeSharedObject = async ( const treeSchema = sharedTree.exportSimpleSchema(); const sf = new SchemaFactory(undefined); - const isLeaf = Tree.is(treeView, [sf.boolean, sf.null, sf.number, sf.handle, sf.string]); - const schemaName = isLeaf ? Tree.schema(treeView).identifier : treeView.type; + const schemaName = Tree.is(treeView, [sf.boolean, sf.null, sf.number, sf.handle, sf.string]) + ? Tree.schema(treeView).identifier + : treeView.type; // Create a root field visualization that shows the allowed types at the root const visualTreeRepresentation: VisualSharedTreeNode = { @@ -283,15 +283,9 @@ export const visualizeSharedTree: VisualizeSharedObject = async ( schemaName, allowedTypes: concatenateTypes(treeSchema.allowedTypes), }, - fields: isLeaf - ? { - root: await visualizeSharedTreeNodeBySchema( - treeView, - treeSchema, - visualizeChildData, - ), - } - : await visualizeVerboseNodeFields(treeView, treeSchema, visualizeChildData), + fields: { + root: await visualizeSharedTreeNodeBySchema(treeView, treeSchema, visualizeChildData), + }, kind: VisualSharedTreeNodeKind.InternalNode, }; diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts index 6c5f81ebca4e..45961ffe9443 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts @@ -171,7 +171,7 @@ function getObjectAllowedTypes(schema: SimpleObjectNodeSchema): string { /** * Returns the schema & fields of the node. */ -export async function visualizeVerboseNodeFields( +async function visualizeVerboseNodeFields( tree: VerboseTreeNode, treeSchema: SimpleTreeSchema, visualizeChildData: VisualizeChildData, From 9a6793aa663869a2f58d6adec27b20c0ea264f79 Mon Sep 17 00:00:00 2001 From: Ji Kim <111468570+jikim-msft@users.noreply.github.com> Date: Thu, 16 Jan 2025 19:11:20 +0000 Subject: [PATCH 4/8] change variable name & remove test --- .../data-visualization/DefaultVisualizers.ts | 10 +- .../src/test/DefaultVisualizers.spec.ts | 314 ------------------ 2 files changed, 8 insertions(+), 316 deletions(-) diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts index e3b2e9de13b5..5673f7cc9531 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts @@ -272,8 +272,14 @@ export const visualizeSharedTree: VisualizeSharedObject = async ( // Schema of the tree node. const treeSchema = sharedTree.exportSimpleSchema(); - const sf = new SchemaFactory(undefined); - const schemaName = Tree.is(treeView, [sf.boolean, sf.null, sf.number, sf.handle, sf.string]) + const schemaFactory = new SchemaFactory(undefined); + const schemaName = Tree.is(treeView, [ + schemaFactory.boolean, + schemaFactory.null, + schemaFactory.number, + schemaFactory.handle, + schemaFactory.string, + ]) ? Tree.schema(treeView).identifier : treeView.type; diff --git a/packages/tools/devtools/devtools-core/src/test/DefaultVisualizers.spec.ts b/packages/tools/devtools/devtools-core/src/test/DefaultVisualizers.spec.ts index 0e9575438591..d532f1a8ddeb 100644 --- a/packages/tools/devtools/devtools-core/src/test/DefaultVisualizers.spec.ts +++ b/packages/tools/devtools/devtools-core/src/test/DefaultVisualizers.spec.ts @@ -1790,320 +1790,6 @@ describe("DefaultVisualizers unit tests", () => { expect(result).to.deep.equal(expected); }); - it.only("SharedTree: Renders multiple allowed types in SharedTree's root field", async () => { - const factory = SharedTree.getFactory(); - const builder = new SchemaFactory("shared-tree-test"); - - const sharedTree = factory.create( - new MockFluidDataStoreRuntime({ idCompressor: createIdCompressor() }), - "test", - ); - - class LeafSchema extends builder.object("leaf-item", { - leafField: [builder.boolean, builder.handle, builder.string], - }) {} - - class ChildSchema extends builder.object("child-item", { - childField: [builder.string, builder.boolean], - childData: builder.optional(LeafSchema), - }) {} - - class RootNodeTwoItemTwo extends builder.object("root-node-two-item-two", { - childrenOne: builder.array(ChildSchema), - childrenTwo: builder.number, - }) {} - - class RootNodeTwoItem extends builder.object("root-node-item", { - childrenOne: builder.number, - childrenTwo: RootNodeTwoItemTwo, - }) {} - - class RootNodeOne extends builder.object("root-node-one", { - leafField: [builder.boolean, builder.handle, builder.string], - }) {} - - class RootNodeTwo extends builder.object("root-node-two", { - childField: RootNodeTwoItem, - }) {} - - const view = sharedTree.viewWith( - new TreeViewConfiguration({ - schema: [RootNodeOne, RootNodeTwo, builder.string, builder.number], - }), - ); - - view.initialize({ - childField: { - childrenOne: 42, - childrenTwo: { - childrenOne: [ - { - childField: false, - childData: { - leafField: "leaf data", - }, - }, - { - childField: true, - }, - ], - childrenTwo: 123, - }, - }, - }); - - const result = await visualizeSharedTree( - sharedTree as unknown as ISharedObject, - visualizeChildData, - ); - - const expected = { - children: { - root: { - children: { - childField: { - children: { - childrenOne: { - value: 42, - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.number", - }, - }, - }, - }, - }, - childrenTwo: { - children: { - childrenOne: { - children: { - "0": { - children: { - childField: { - value: false, - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.boolean", - }, - }, - }, - }, - }, - childData: { - children: { - leafField: { - value: "leaf data", - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.string", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.leaf-item", - }, - allowedTypes: { - value: - "{ leafField : com.fluidframework.leaf.boolean | com.fluidframework.leaf.handle | com.fluidframework.leaf.string }", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.child-item", - }, - allowedTypes: { - value: - "{ childField : com.fluidframework.leaf.string | com.fluidframework.leaf.boolean, childData : shared-tree-test.leaf-item }", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - "1": { - children: { - childField: { - value: true, - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.boolean", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.child-item", - }, - allowedTypes: { - value: - "{ childField : com.fluidframework.leaf.string | com.fluidframework.leaf.boolean, childData : shared-tree-test.leaf-item }", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: 'shared-tree-test.Array<["shared-tree-test.child-item"]>', - }, - allowedTypes: { - value: "shared-tree-test.child-item", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - childrenTwo: { - value: 123, - nodeKind: "ValueNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "com.fluidframework.leaf.number", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.root-node-two-item-two", - }, - allowedTypes: { - value: - '{ childrenOne : shared-tree-test.Array<["shared-tree-test.child-item"]>, childrenTwo : com.fluidframework.leaf.number }', - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.root-node-item", - }, - allowedTypes: { - value: - "{ childrenOne : com.fluidframework.leaf.number, childrenTwo : shared-tree-test.root-node-two-item-two }", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - }, - nodeKind: "TreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.root-node-two", - }, - allowedTypes: { - value: "{ childField : shared-tree-test.root-node-item }", - nodeKind: "ValueNode", - }, - }, - }, - }, - }, - }, - nodeKind: "FluidTreeNode", - tooltipContents: { - schema: { - nodeKind: "TreeNode", - children: { - name: { - nodeKind: "ValueNode", - value: "shared-tree-test.root-node-two", - }, - allowedTypes: { - value: - "shared-tree-test.root-node-one | shared-tree-test.root-node-two | com.fluidframework.leaf.string | com.fluidframework.leaf.number", - nodeKind: "ValueNode", - }, - }, - }, - }, - fluidObjectId: "test", - typeMetadata: "SharedTree", - }; - - expect(result).to.deep.equal(expected); - }); - it("Unknown SharedObject", async () => { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const unknownObject = { From d4c49019a9ae7101077bc9f364cbbc97e72506be Mon Sep 17 00:00:00 2001 From: Ji Kim <111468570+jikim-msft@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:38:51 +0000 Subject: [PATCH 5/8] Co-authored-by: Joshua Smithrud --- .../data-visualization/DefaultVisualizers.ts | 4 +- .../SharedTreeVisualizer.ts | 70 ++++++++++++------- 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts index 5673f7cc9531..7237836572cf 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts @@ -31,7 +31,7 @@ import { concatenateTypes, determineNodeKind, toVisualTree, - visualizeSharedTreeNodeBySchema, + visualizeSharedTreeBySchema, } from "./SharedTreeVisualizer.js"; import { VisualSharedTreeNodeKind, @@ -290,7 +290,7 @@ export const visualizeSharedTree: VisualizeSharedObject = async ( allowedTypes: concatenateTypes(treeSchema.allowedTypes), }, fields: { - root: await visualizeSharedTreeNodeBySchema(treeView, treeSchema, visualizeChildData), + root: await visualizeSharedTreeBySchema(treeView, treeSchema, visualizeChildData), }, kind: VisualSharedTreeNodeKind.InternalNode, }; diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts index 45961ffe9443..95ceba7bcb65 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts @@ -172,16 +172,14 @@ function getObjectAllowedTypes(schema: SimpleObjectNodeSchema): string { * Returns the schema & fields of the node. */ async function visualizeVerboseNodeFields( - tree: VerboseTreeNode, + treeFields: VerboseTree[] | Record, treeSchema: SimpleTreeSchema, visualizeChildData: VisualizeChildData, ): Promise> { - const treeFields = tree.fields; - const fields: Record = {}; for (const [fieldKey, childField] of Object.entries(treeFields)) { - fields[fieldKey] = await visualizeSharedTreeNodeBySchema( + fields[fieldKey] = await visualizeSharedTreeBySchema( childField, treeSchema, visualizeChildData, @@ -205,7 +203,7 @@ async function visualizeObjectNode( schemaName: tree.type, allowedTypes: getObjectAllowedTypes(nodeSchema), }, - fields: await visualizeVerboseNodeFields(tree, treeSchema, visualizeChildData), + fields: await visualizeVerboseNodeFields(tree.fields, treeSchema, visualizeChildData), kind: VisualSharedTreeNodeKind.InternalNode, }; } @@ -224,36 +222,25 @@ async function visualizeMapNode( schemaName: tree.type, allowedTypes: `Record`, }, - fields: await visualizeVerboseNodeFields(tree, treeSchema, visualizeChildData), + fields: await visualizeVerboseNodeFields(tree.fields, treeSchema, visualizeChildData), kind: VisualSharedTreeNodeKind.InternalNode, }; } /** - * Main recursive helper function to create the visual representation of the SharedTree. - * Processes tree nodes based on their schema type (e.g., ObjectNodeStoredSchema, MapNodeStoredSchema, LeafNodeStoredSchema), producing the visual representation for each type. + * Helper function to create the visual representation of non-leaf SharedTree nodes. + * Processes internal tree nodes based on their schema type (e.g., ObjectNodeStoredSchema, MapNodeStoredSchema, ArrayNodeStoredSchema), + * producing the visual representation for each type. * * @see {@link https://fluidframework.com/docs/data-structures/tree/} for more information on the SharedTree schema. * * @remarks */ -export async function visualizeSharedTreeNodeBySchema( - tree: VerboseTree, +async function visualizeSharedTreeNodeBySchema( + tree: VerboseTreeNode, treeSchema: SimpleTreeSchema, visualizeChildData: VisualizeChildData, ): Promise { - const sf = new SchemaFactory(undefined); - if (Tree.is(tree, [sf.boolean, sf.null, sf.number, sf.handle, sf.string])) { - const nodeSchema = Tree.schema(tree); - return { - schema: { - schemaName: nodeSchema.identifier, - }, - value: await visualizeChildData(tree), - kind: VisualSharedTreeNodeKind.LeafNode, - }; - } - const schema = treeSchema.definitions.get(tree.type); if (schema === undefined) { throw new TypeError("Unrecognized schema type."); @@ -281,7 +268,7 @@ export async function visualizeSharedTreeNodeBySchema( } for (let i = 0; i < children.length; i++) { - fields[i] = await visualizeSharedTreeNodeBySchema( + fields[i] = await visualizeSharedTreeBySchema( children[i], treeSchema, visualizeChildData, @@ -293,7 +280,7 @@ export async function visualizeSharedTreeNodeBySchema( schemaName: tree.type, allowedTypes: concatenateTypes(schema.allowedTypes), }, - fields: await visualizeVerboseNodeFields(tree, treeSchema, visualizeChildData), + fields: await visualizeVerboseNodeFields(tree.fields, treeSchema, visualizeChildData), kind: VisualSharedTreeNodeKind.InternalNode, }; } @@ -302,3 +289,38 @@ export async function visualizeSharedTreeNodeBySchema( } } } + +/** + * Creates a visual representation of a SharedTree based on its schema. + * @param tree - The {@link VerboseTree} to visualize + * @param treeSchema - The schema that defines the structure and types of the tree + * @param visualizeChildData - Callback function to visualize child node data + * @returns A visual representation of the tree that includes schema information and node values + * + * @remarks + * This function handles both leaf nodes (primitive values, handles) and internal nodes (objects, maps, arrays). + * For leaf nodes, it creates a visual representation with the node's schema and value. + * For internal nodes, it recursively processes the node's fields using {@link visualizeSharedTreeNodeBySchema}. + */ +export async function visualizeSharedTreeBySchema( + tree: VerboseTree, + treeSchema: SimpleTreeSchema, + visualizeChildData: VisualizeChildData, +): Promise { + const schemaFactory = new SchemaFactory(undefined); + return Tree.is(tree, [ + schemaFactory.boolean, + schemaFactory.null, + schemaFactory.number, + schemaFactory.handle, + schemaFactory.string, + ]) + ? { + schema: { + schemaName: Tree.schema(tree).identifier, + }, + value: await visualizeChildData(tree), + kind: VisualSharedTreeNodeKind.LeafNode, + } + : visualizeSharedTreeNodeBySchema(tree, treeSchema, visualizeChildData); +} From 4990a601ce07572d8393e3cff921fe798902ee60 Mon Sep 17 00:00:00 2001 From: Ji Kim <111468570+jikim-msft@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:41:42 +0000 Subject: [PATCH 6/8] change name --- .../src/data-visualization/SharedTreeVisualizer.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts index 95ceba7bcb65..4354a81cafac 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts @@ -91,7 +91,7 @@ function createToolTipContents(schema: SharedTreeSchemaNode): VisualTreeNode { } /** - * Converts the visual representation from {@link visualizeSharedTreeNodeBySchema} to a visual tree compatible with the devtools-view. + * Converts the visual representation from {@link visualizeInternalNodeBySchema} to a visual tree compatible with the devtools-view. * @param tree - the visual representation of the SharedTree. * @returns - the visual representation of type {@link VisualChildNode} */ @@ -236,7 +236,7 @@ async function visualizeMapNode( * * @remarks */ -async function visualizeSharedTreeNodeBySchema( +async function visualizeInternalNodeBySchema( tree: VerboseTreeNode, treeSchema: SimpleTreeSchema, visualizeChildData: VisualizeChildData, @@ -300,7 +300,7 @@ async function visualizeSharedTreeNodeBySchema( * @remarks * This function handles both leaf nodes (primitive values, handles) and internal nodes (objects, maps, arrays). * For leaf nodes, it creates a visual representation with the node's schema and value. - * For internal nodes, it recursively processes the node's fields using {@link visualizeSharedTreeNodeBySchema}. + * For internal nodes, it recursively processes the node's fields using {@link visualizeInternalNodeBySchema}. */ export async function visualizeSharedTreeBySchema( tree: VerboseTree, @@ -322,5 +322,5 @@ export async function visualizeSharedTreeBySchema( value: await visualizeChildData(tree), kind: VisualSharedTreeNodeKind.LeafNode, } - : visualizeSharedTreeNodeBySchema(tree, treeSchema, visualizeChildData); + : visualizeInternalNodeBySchema(tree, treeSchema, visualizeChildData); } From 363ced9e95552ab321e1048499e713e52eab159a Mon Sep 17 00:00:00 2001 From: Ji Kim <111468570+jikim-msft@users.noreply.github.com> Date: Fri, 17 Jan 2025 20:00:56 +0000 Subject: [PATCH 7/8] handle undefined --- .../src/data-visualization/DefaultVisualizers.ts | 8 +++++++- .../tools/devtools/devtools-test-app/src/FluidObject.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts index 7237836572cf..d8a659023d5b 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts @@ -265,8 +265,14 @@ export const visualizeSharedTree: VisualizeSharedObject = async ( // Root node of the SharedTree's content. const treeView = sharedTree.exportVerbose(); + if (treeView === undefined) { - throw new Error("Support for visualizing empty trees is not implemented"); + return { + fluidObjectId: sharedTree.id, + typeMetadata: "SharedTree", + nodeKind: VisualNodeKind.FluidTreeNode, + children: {}, + }; } // Schema of the tree node. diff --git a/packages/tools/devtools/devtools-test-app/src/FluidObject.ts b/packages/tools/devtools/devtools-test-app/src/FluidObject.ts index fdc16fa42a9c..6c41592b6a39 100644 --- a/packages/tools/devtools/devtools-test-app/src/FluidObject.ts +++ b/packages/tools/devtools/devtools-test-app/src/FluidObject.ts @@ -204,7 +204,7 @@ export class AppData extends DataObject { }) {} class RootNodeTwo extends builder.object("root-node-two", { - childField: RootNodeTwoItem, + childField: builder.optional(RootNodeTwoItem), }) {} const config = new TreeViewConfiguration({ From dbf8f9bf39eb41cd22d4748f0a6da0c05036a6cc Mon Sep 17 00:00:00 2001 From: Ji Kim <111468570+jikim-msft@users.noreply.github.com> Date: Fri, 17 Jan 2025 23:19:05 +0000 Subject: [PATCH 8/8] refactor / undefined --- .../data-visualization/DefaultVisualizers.ts | 35 +++++-------------- .../SharedTreeVisualizer.ts | 12 ++++++- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts index d8a659023d5b..eeab5ec91adf 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts @@ -22,21 +22,17 @@ import { SharedMatrix } from "@fluidframework/matrix/internal"; import { SharedString } from "@fluidframework/sequence/internal"; import type { ISharedObject } from "@fluidframework/shared-object-base/internal"; import type { ITreeInternal } from "@fluidframework/tree/internal"; -import { SchemaFactory, SharedTree, Tree } from "@fluidframework/tree/internal"; +import { SharedTree } from "@fluidframework/tree/internal"; import { EditType } from "../CommonInterfaces.js"; import type { VisualizeChildData, VisualizeSharedObject } from "./DataVisualization.js"; import { - concatenateTypes, determineNodeKind, toVisualTree, visualizeSharedTreeBySchema, } from "./SharedTreeVisualizer.js"; -import { - VisualSharedTreeNodeKind, - type VisualSharedTreeNode, -} from "./VisualSharedTreeTypes.js"; +import type { VisualSharedTreeNode } from "./VisualSharedTreeTypes.js"; import { type FluidObjectNode, type FluidObjectTreeNode, @@ -278,28 +274,13 @@ export const visualizeSharedTree: VisualizeSharedObject = async ( // Schema of the tree node. const treeSchema = sharedTree.exportSimpleSchema(); - const schemaFactory = new SchemaFactory(undefined); - const schemaName = Tree.is(treeView, [ - schemaFactory.boolean, - schemaFactory.null, - schemaFactory.number, - schemaFactory.handle, - schemaFactory.string, - ]) - ? Tree.schema(treeView).identifier - : treeView.type; - // Create a root field visualization that shows the allowed types at the root - const visualTreeRepresentation: VisualSharedTreeNode = { - schema: { - schemaName, - allowedTypes: concatenateTypes(treeSchema.allowedTypes), - }, - fields: { - root: await visualizeSharedTreeBySchema(treeView, treeSchema, visualizeChildData), - }, - kind: VisualSharedTreeNodeKind.InternalNode, - }; + const visualTreeRepresentation: VisualSharedTreeNode = await visualizeSharedTreeBySchema( + treeView, + treeSchema, + visualizeChildData, + true, + ); // Maps the `visualTreeRepresentation` in the format compatible to {@link visualizeChildData} function. const visualTree = toVisualTree(visualTreeRepresentation); diff --git a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts index 4354a81cafac..08fbd2df7526 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/SharedTreeVisualizer.ts @@ -198,10 +198,14 @@ async function visualizeObjectNode( treeSchema: SimpleTreeSchema, visualizeChildData: VisualizeChildData, ): Promise { + const isRootField = treeSchema.allowedTypes.has(tree.type); + return { schema: { schemaName: tree.type, - allowedTypes: getObjectAllowedTypes(nodeSchema), + allowedTypes: isRootField + ? concatenateTypes(treeSchema.allowedTypes) + : getObjectAllowedTypes(nodeSchema), }, fields: await visualizeVerboseNodeFields(tree.fields, treeSchema, visualizeChildData), kind: VisualSharedTreeNodeKind.InternalNode, @@ -306,8 +310,10 @@ export async function visualizeSharedTreeBySchema( tree: VerboseTree, treeSchema: SimpleTreeSchema, visualizeChildData: VisualizeChildData, + isRootField?: boolean, ): Promise { const schemaFactory = new SchemaFactory(undefined); + return Tree.is(tree, [ schemaFactory.boolean, schemaFactory.null, @@ -318,6 +324,10 @@ export async function visualizeSharedTreeBySchema( ? { schema: { schemaName: Tree.schema(tree).identifier, + allowedTypes: + isRootField === true + ? concatenateTypes(treeSchema.allowedTypes) + : Tree.schema(tree).identifier, }, value: await visualizeChildData(tree), kind: VisualSharedTreeNodeKind.LeafNode,