Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(devtools-core): Render ST's Visualizer to Include Root Field's Allowed Types #23573

Open
wants to merge 49 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
df3da17
include allowedTypes for root field
jikim-msft Jan 16, 2025
fd50e40
add condition
jikim-msft Jan 16, 2025
cf42f7a
Revert "add condition"
jikim-msft Jan 16, 2025
1e29755
Merge branch 'main' into devtools/shared-tree-visualizer
jikim-msft Jan 16, 2025
5e96b73
Merge branch 'main' into devtools/shared-tree-visualizer
jikim-msft Jan 16, 2025
9a6793a
change variable name & remove test
jikim-msft Jan 16, 2025
d4c4901
Co-authored-by: Joshua Smithrud <[email protected]>
jikim-msft Jan 17, 2025
4990a60
change name
jikim-msft Jan 17, 2025
363ced9
handle undefined
jikim-msft Jan 17, 2025
dbf8f9b
refactor / undefined
jikim-msft Jan 17, 2025
d80f2e2
add allowedtypes
jikim-msft Jan 24, 2025
3159b80
fix schema name / remove console
jikim-msft Jan 25, 2025
b40de03
fix doc
jikim-msft Jan 25, 2025
13ee23d
fix test
jikim-msft Jan 25, 2025
a861e7e
Merge branch 'main' into devtools/shared-tree-visualizer
jikim-msft Jan 25, 2025
0746731
make example schema more concise
jikim-msft Jan 27, 2025
f02d1a3
revertible: adds kind
jikim-msft Jan 28, 2025
a77bf59
add requirement
jikim-msft Jan 28, 2025
9132cbb
change name & schema
jikim-msft Jan 29, 2025
4e86139
change name & schema
jikim-msft Jan 29, 2025
08db65a
fix varaible name / add undefined
jikim-msft Jan 29, 2025
e8c06bf
change test
jikim-msft Jan 29, 2025
96a015b
improve doc
jikim-msft Jan 29, 2025
18fbeac
change
jikim-msft Jan 30, 2025
3bc38fb
change param type
jikim-msft Jan 30, 2025
542d26e
changed to map
jikim-msft Jan 30, 2025
279031d
fix doc
jikim-msft Jan 30, 2025
9219ae1
REVERTIBLE: add undefined case
jikim-msft Jan 30, 2025
2d305c6
omit schema for undefined
jikim-msft Jan 31, 2025
a3978ab
Merge branch 'main' into devtools/shared-tree-visualizer
jikim-msft Jan 31, 2025
a284f3a
change fix
jikim-msft Jan 31, 2025
bffadbe
Merge branch 'main' into devtools/shared-tree-visualizer
jikim-msft Feb 1, 2025
f44bd59
Update packages/tools/devtools/devtools-core/src/data-visualization/D…
jikim-msft Feb 4, 2025
7d389dc
Update packages/tools/devtools/devtools-core/src/data-visualization/S…
jikim-msft Feb 4, 2025
89d0365
Update packages/tools/devtools/devtools-core/src/data-visualization/S…
jikim-msft Feb 4, 2025
035c070
add doc & couple the parameters & add readonly
jikim-msft Feb 4, 2025
fdfa737
change
jikim-msft Feb 4, 2025
446ba37
Merge branch 'main' into devtools/shared-tree-visualizer
jikim-msft Feb 4, 2025
aeee9a2
change type
jikim-msft Feb 4, 2025
dfc6f00
change param
jikim-msft Feb 4, 2025
0f70aae
Revert "change param"
jikim-msft Feb 4, 2025
3fa33c2
Update packages/tools/devtools/devtools-core/src/data-visualization/V…
jikim-msft Feb 7, 2025
809f3fe
Update packages/tools/devtools/devtools-core/src/data-visualization/D…
jikim-msft Feb 7, 2025
472aa64
Update packages/tools/devtools/devtools-core/src/data-visualization/S…
jikim-msft Feb 7, 2025
8a650d1
fix nit
jikim-msft Feb 7, 2025
1aa4c02
Merge branch 'devtools/shared-tree-visualizer' of https://github.com/…
jikim-msft Feb 7, 2025
c6519df
Merge branch 'main' into devtools/shared-tree-visualizer
jikim-msft Feb 7, 2025
8fb3ca9
change tree api
jikim-msft Feb 7, 2025
7413066
change name
jikim-msft Feb 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ import { EditType } from "../CommonInterfaces.js";

import type { VisualizeChildData, VisualizeSharedObject } from "./DataVisualization.js";
import {
concatenateTypes,
determineNodeKind,
toVisualTree,
visualizeSharedTreeNodeBySchema,
visualizeSharedTreeBySchema,
} from "./SharedTreeVisualizer.js";
import type { VisualSharedTreeNode } from "./VisualSharedTreeTypes.js";
import {
type FluidObjectNode,
type FluidObjectTreeNode,
Expand Down Expand Up @@ -260,26 +262,37 @@ 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 {
jikim-msft marked this conversation as resolved.
Show resolved Hide resolved
jikim-msft marked this conversation as resolved.
Show resolved Hide resolved
fluidObjectId: sharedTree.id,
typeMetadata: "SharedTree",
nodeKind: VisualNodeKind.FluidTreeNode,
children: {},
};
}

// Schema of the tree node.
const treeSchema = sharedTree.exportSimpleSchema();
const treeDefinitions = sharedTree.exportSimpleSchema().definitions;

/**
* {@link visualizeSharedTreeBySchema} passes `allowedTypes` into co-recursive functions while constructing the visual representation.
* Since the {@link SimpleTreeSchema.allowedTypes} of each children node is only accessible at the parent field level,
* each node's allowed types are computed at the parent field level.
*/
const allowedTypes = concatenateTypes(sharedTree.exportSimpleSchema().allowedTypes);

// 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,
treeDefinitions,
allowedTypes,
visualizeChildData,
);

// Maps the `visualTreeRepresentation` in the format compatible to {@link visualizeChildData} function.
const visualTree = toVisualTree(visualTreeRepresentation);

// TODO: Validate the type casting.
jikim-msft marked this conversation as resolved.
Show resolved Hide resolved
const visualTreeResult: FluidObjectNode = {
...visualTree,
fluidObjectId: sharedTree.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

import type {
SimpleMapNodeSchema,
SimpleNodeSchema,
SimpleObjectNodeSchema,
SimpleTreeSchema,
VerboseTree,
VerboseTreeNode,
} from "@fluidframework/tree/internal";
Expand Down Expand Up @@ -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}
*/
Expand Down Expand Up @@ -150,62 +150,72 @@ export function toVisualTree(tree: VisualSharedTreeNode): VisualChildNode {
/**
* Concatenrate allowed types for `ObjectNodeStoredSchema` and `MapNodeStoredSchema`.
*/
function concatenateTypes(fieldTypes: ReadonlySet<string>): string {
export function concatenateTypes(fieldTypes: ReadonlySet<string>): string {
return [...fieldTypes].join(" | ");
}

/**
* Returns the allowed fields & types for the object fields (e.g., `foo : string | number, bar: boolean`)
*/
function getObjectAllowedTypes(schema: SimpleObjectNodeSchema): string {
const result: string[] = [];

for (const [fieldKey, treeFieldSimpleSchema] of Object.entries(schema.fields)) {
const fieldTypes = treeFieldSimpleSchema.allowedTypes;
result.push(`${fieldKey} : ${concatenateTypes(fieldTypes)}`);
}

return `{ ${result.join(", ")} }`;
}

/**
* Returns the schema & fields of the node.
*/
async function visualizeVerboseNodeFields(
tree: VerboseTreeNode,
treeSchema: SimpleTreeSchema,
treeFields: VerboseTree[] | Record<string, VerboseTree>,
jikim-msft marked this conversation as resolved.
Show resolved Hide resolved
treeDefinitions: ReadonlyMap<string, SimpleNodeSchema>,
allowedTypes: Record<string, string>,
visualizeChildData: VisualizeChildData,
): Promise<Record<string, VisualSharedTreeNode>> {
const treeFields = tree.fields;

const fields: Record<string | number, VisualSharedTreeNode> = {};

for (const [fieldKey, childField] of Object.entries(treeFields)) {
fields[fieldKey] = await visualizeSharedTreeNodeBySchema(
fields[fieldKey] = await visualizeSharedTreeBySchema(
childField,
treeSchema,
treeDefinitions,
allowedTypes[fieldKey],
visualizeChildData,
);
}

return fields;
}

function storeObjectAllowedTypes(schema: SimpleObjectNodeSchema): Record<string, string> {
const result: Record<string, string> = {};

for (const [fieldKey, treeFieldSimpleSchema] of Object.entries(schema.fields)) {
const fieldTypes = treeFieldSimpleSchema.allowedTypes;
result[fieldKey] = concatenateTypes(fieldTypes);
}

return result;
}

/**
* Returns the schema & fields of the node with type {@link ObjectNodeStoredSchema}.
*/
async function visualizeObjectNode(
tree: VerboseTreeNode,
nodeSchema: SimpleObjectNodeSchema,
treeSchema: SimpleTreeSchema,
treeDefinitions: ReadonlyMap<string, SimpleNodeSchema>,
allowedTypes: string,
visualizeChildData: VisualizeChildData,
): Promise<VisualSharedTreeNode> {
const objectAllowedTypes = storeObjectAllowedTypes(
treeDefinitions.get(tree.type) as SimpleObjectNodeSchema,
);

return {
schema: {
schemaName: tree.type,
allowedTypes: getObjectAllowedTypes(nodeSchema),
allowedTypes,
},
fields: await visualizeVerboseNodeFields(tree, treeSchema, visualizeChildData),
fields: await visualizeVerboseNodeFields(
tree.fields,
treeDefinitions,
objectAllowedTypes,
visualizeChildData,
),
kind: VisualSharedTreeNodeKind.InternalNode,
};
}
Expand All @@ -216,45 +226,47 @@ async function visualizeObjectNode(
async function visualizeMapNode(
tree: VerboseTreeNode,
nodeSchema: SimpleMapNodeSchema,
treeSchema: SimpleTreeSchema,
treeDefinitions: ReadonlyMap<string, SimpleNodeSchema>,
allowedTypes: string,
visualizeChildData: VisualizeChildData,
): Promise<VisualSharedTreeNode> {
const mapAllowedTypes: Record<string, string> = {};

for (const key of Object.keys(tree.fields)) {
mapAllowedTypes[key] = concatenateTypes(nodeSchema.allowedTypes);
}
return {
schema: {
schemaName: tree.type,
allowedTypes: `Record<string, ${concatenateTypes(nodeSchema.allowedTypes)}>`,
allowedTypes,
},
fields: await visualizeVerboseNodeFields(tree, treeSchema, visualizeChildData),
fields: await visualizeVerboseNodeFields(
tree.fields,
treeDefinitions,
mapAllowedTypes,
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.
jikim-msft marked this conversation as resolved.
Show resolved Hide resolved
* 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,
treeSchema: SimpleTreeSchema,
async function visualizeInternalNodeBySchema(
tree: VerboseTreeNode,
treeDefinitions: ReadonlyMap<string, SimpleNodeSchema>,
allowedTypes: string,
visualizeChildData: VisualizeChildData,
): Promise<VisualSharedTreeNode> {
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 = treeDefinitions.get(tree.type);

const schema = treeSchema.definitions.get(tree.type);
if (schema === undefined) {
throw new TypeError("Unrecognized schema type.");
}
Expand All @@ -263,14 +275,20 @@ export async function visualizeSharedTreeNodeBySchema(
case NodeKind.Object: {
const objectVisualized = visualizeObjectNode(
tree,
schema,
treeSchema,
treeDefinitions,
allowedTypes,
visualizeChildData,
);
return objectVisualized;
}
case NodeKind.Map: {
const mapVisualized = visualizeMapNode(tree, schema, treeSchema, visualizeChildData);
const mapVisualized = visualizeMapNode(
tree,
schema,
treeDefinitions,
allowedTypes,
visualizeChildData,
);
return mapVisualized;
}
case NodeKind.Array: {
Expand All @@ -280,20 +298,29 @@ export async function visualizeSharedTreeNodeBySchema(
throw new TypeError("Invalid array");
}

const arrayAllowedTypes: Record<string, string> = {};
for (let i = 0; i < children.length; i++) {
fields[i] = await visualizeSharedTreeNodeBySchema(
arrayAllowedTypes[i] = concatenateTypes(schema.allowedTypes);

fields[i] = await visualizeSharedTreeBySchema(
children[i],
treeSchema,
treeDefinitions,
arrayAllowedTypes[i],
visualizeChildData,
);
}

return {
schema: {
schemaName: tree.type,
allowedTypes: concatenateTypes(schema.allowedTypes),
allowedTypes,
},
fields: await visualizeVerboseNodeFields(tree, treeSchema, visualizeChildData),
fields: await visualizeVerboseNodeFields(
tree.fields,
treeDefinitions,
arrayAllowedTypes,
visualizeChildData,
),
kind: VisualSharedTreeNodeKind.InternalNode,
};
}
Expand All @@ -302,3 +329,41 @@ 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,
treeDefinitions: ReadonlyMap<string, SimpleNodeSchema>,
allowedTypes: string,
jikim-msft marked this conversation as resolved.
Show resolved Hide resolved
visualizeChildData: VisualizeChildData,
): Promise<VisualSharedTreeNode> {
const schemaFactory = new SchemaFactory(undefined);

return Tree.is(tree, [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just double-checking: is there an existing "isLeaf" (or something) helper we could use for this check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will double check on this. Thanks for the reminder.

Copy link
Contributor Author

@jikim-msft jikim-msft Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a treeValue() utiliy function that is consumed internally in the ST package, but I am not sure if I can expose this to outside package.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that function doesn't handle Handle nodes.

@CraigMacomber maybe you can advise on the best publicly available pattern for checking if a node is a leaf using the VerboseTree APIs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since leaves in VerboseTree are the same as leaves in regular trees. you can do something like:

const f = new SchemaFactory(undefined);
const leaf = [f.number, f.string, f.null, f.handle, f.boolean];
Tree.is(node, leaf);
``

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I expect us to add a new leaf type anytime soon, but it seems like it would be nice to have this sort of check encapsulated in a single place so we don't have to update all of these hardcoded lists if/when we do add a new leaf type. Not for this PR. but would it be worthwhile to add a centralized isLeaf function for this?

schemaFactory.boolean,
schemaFactory.null,
schemaFactory.number,
schemaFactory.handle,
schemaFactory.string,
])
? {
schema: {
schemaName: Tree.schema(tree).identifier,
allowedTypes,
},
value: await visualizeChildData(tree),
kind: VisualSharedTreeNodeKind.LeafNode,
}
: visualizeInternalNodeBySchema(tree, treeDefinitions, allowedTypes, visualizeChildData);
}
Loading
Loading