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..eeab5ec91adf 100644 --- a/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts +++ b/packages/tools/devtools/devtools-core/src/data-visualization/DefaultVisualizers.ts @@ -30,8 +30,9 @@ import type { VisualizeChildData, VisualizeSharedObject } from "./DataVisualizat import { determineNodeKind, toVisualTree, - visualizeSharedTreeNodeBySchema, + visualizeSharedTreeBySchema, } from "./SharedTreeVisualizer.js"; +import type { VisualSharedTreeNode } from "./VisualSharedTreeTypes.js"; import { type FluidObjectNode, type FluidObjectTreeNode, @@ -260,26 +261,30 @@ 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"); + return { + fluidObjectId: sharedTree.id, + typeMetadata: "SharedTree", + nodeKind: VisualNodeKind.FluidTreeNode, + children: {}, + }; } // 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( + // Create a root field visualization that shows the allowed types at the root + const visualTreeRepresentation: VisualSharedTreeNode = await visualizeSharedTreeBySchema( treeView, treeSchema, visualizeChildData, + true, ); // 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..08fbd2df7526 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} */ @@ -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(" | "); } @@ -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, @@ -200,12 +198,16 @@ 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, treeSchema, visualizeChildData), + fields: await visualizeVerboseNodeFields(tree.fields, treeSchema, visualizeChildData), kind: VisualSharedTreeNodeKind.InternalNode, }; } @@ -224,36 +226,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 visualizeInternalNodeBySchema( + 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 +272,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 +284,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 +293,44 @@ 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 visualizeInternalNodeBySchema}. + */ +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, + schemaFactory.number, + schemaFactory.handle, + schemaFactory.string, + ]) + ? { + schema: { + schemaName: Tree.schema(tree).identifier, + allowedTypes: + isRootField === true + ? concatenateTypes(treeSchema.allowedTypes) + : Tree.schema(tree).identifier, + }, + value: await visualizeChildData(tree), + kind: VisualSharedTreeNodeKind.LeafNode, + } + : visualizeInternalNodeBySchema(tree, treeSchema, visualizeChildData); +} 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..d532f1a8ddeb 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,66 @@ 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("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..6c41592b6a39 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: builder.optional(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, + }, }); } }