diff --git a/diagram-editor/frontend/api.preprocessed.schema.json b/diagram-editor/frontend/api.preprocessed.schema.json
index d600e125..d899484f 100644
--- a/diagram-editor/frontend/api.preprocessed.schema.json
+++ b/diagram-editor/frontend/api.preprocessed.schema.json
@@ -114,6 +114,22 @@
}
]
},
+ "ConfigExample": {
+ "properties": {
+ "config": {
+ "description": "The value of the config"
+ },
+ "description": {
+ "description": "A description of what this config is for",
+ "type": "string"
+ }
+ },
+ "required": [
+ "description",
+ "config"
+ ],
+ "type": "object"
+ },
"DebugSessionMessage": {
"oneOf": [
{
@@ -780,6 +796,18 @@
"description": "If the user does not specify a default display text, the node ID will\n be used here.",
"type": "string"
},
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "example_configs": {
+ "items": {
+ "$ref": "#/$defs/ConfigExample"
+ },
+ "type": "array"
+ },
"request": {
"type": "string"
},
@@ -798,7 +826,8 @@
"request",
"response",
"streams",
- "config_schema"
+ "config_schema",
+ "example_configs"
],
"type": "object"
},
@@ -1027,6 +1056,18 @@
"default_display_text": {
"type": "string"
},
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "example_configs": {
+ "items": {
+ "$ref": "#/$defs/ConfigExample"
+ },
+ "type": "array"
+ },
"metadata": {
"$ref": "#/$defs/SectionMetadata"
}
@@ -1034,7 +1075,8 @@
"required": [
"default_display_text",
"metadata",
- "config_schema"
+ "config_schema",
+ "example_configs"
],
"type": "object"
},
diff --git a/diagram-editor/frontend/forms/node-form.tsx b/diagram-editor/frontend/forms/node-form.tsx
index a7e94920..d4411e81 100644
--- a/diagram-editor/frontend/forms/node-form.tsx
+++ b/diagram-editor/frontend/forms/node-form.tsx
@@ -1,5 +1,13 @@
-import { Autocomplete, TextField } from '@mui/material';
+import {
+ Autocomplete,
+ Box,
+ ListItem,
+ Stack,
+ TextField,
+ Tooltip,
+} from '@mui/material';
import { useMemo, useState } from 'react';
+import { MaterialSymbol } from '../nodes';
import { useRegistry } from '../registry-provider';
import BaseEditOperationForm, {
type BaseEditOperationFormProps,
@@ -60,6 +68,25 @@ function NodeForm(props: NodeFormProps) {
renderInput={(params) => (
)}
+ renderOption={({ key, ...otherProps }, value) => {
+ const nodeMetadata = registry.nodes[value];
+ return (
+
+
+ {value}
+ {nodeMetadata?.description && (
+
+
+
+ )}
+
+
+ );
+ }}
/>
+
{symbol}
);
diff --git a/diagram-editor/frontend/types/api.d.ts b/diagram-editor/frontend/types/api.d.ts
index 816b95b4..87f94e8d 100644
--- a/diagram-editor/frontend/types/api.d.ts
+++ b/diagram-editor/frontend/types/api.d.ts
@@ -352,6 +352,23 @@ export interface BufferSettings {
retention: RetentionPolicy;
[k: string]: unknown;
}
+/**
+ * This interface was referenced by `DiagramEditorApi`'s JSON-Schema
+ * via the `definition` "ConfigExample".
+ */
+export interface ConfigExample {
+ /**
+ * The value of the config
+ */
+ config: {
+ [k: string]: unknown;
+ };
+ /**
+ * A description of what this config is for
+ */
+ description: string;
+ [k: string]: unknown;
+}
/**
* This interface was referenced by `DiagramEditorApi`'s JSON-Schema
* via the `definition` "Diagram".
@@ -1111,6 +1128,8 @@ export interface NodeRegistration {
* be used here.
*/
default_display_text: string;
+ description?: string | null;
+ example_configs: ConfigExample[];
request: string;
response: string;
streams: {
@@ -1125,6 +1144,8 @@ export interface NodeRegistration {
export interface SectionRegistration {
config_schema: Schema;
default_display_text: string;
+ description?: string | null;
+ example_configs: ConfigExample[];
metadata: SectionMetadata;
[k: string]: unknown;
}
diff --git a/examples/diagram/calculator/diagrams/split_and_join.json b/examples/diagram/calculator/diagrams/split_and_join.json
index 9028ccd2..87403f1c 100644
--- a/examples/diagram/calculator/diagrams/split_and_join.json
+++ b/examples/diagram/calculator/diagrams/split_and_join.json
@@ -8,14 +8,16 @@
"type": "node",
"builder": "mul",
"next": "a00aa305-9763-4602-b638-6cb190e6c452",
- "config": 10
+ "config": 100,
+ "display_text": "x100"
},
"fee7f385-2a74-4a87-b8a7-87b8bd03fdf8": {
"type": "node",
"builder": "add",
"next": {
"builtin": "terminate"
- }
+ },
+ "display_text": "sum"
},
"31fb7423-5300-447a-b259-49c5c79654e7": {
"type": "join",
@@ -29,7 +31,8 @@
"type": "node",
"builder": "mul",
"next": "305f46be-cf0d-42ac-bd28-26d63746abc3",
- "config": 100
+ "config": 10,
+ "display_text": "x10"
},
"74764679-1f94-49f3-8080-259e57f78be9": {
"type": "split",
diff --git a/examples/diagram/calculator_ops_catalog/src/lib.rs b/examples/diagram/calculator_ops_catalog/src/lib.rs
index 09409f0f..f128f9de 100644
--- a/examples/diagram/calculator_ops_catalog/src/lib.rs
+++ b/examples/diagram/calculator_ops_catalog/src/lib.rs
@@ -1,9 +1,11 @@
use std::fmt::Write;
-use bevy_impulse::{AsyncMap, DiagramElementRegistry, JsonMessage, NodeBuilderOptions, StreamPack};
+use bevy_impulse::{
+ AsyncMap, ConfigExample, DiagramElementRegistry, JsonMessage, NodeBuilderOptions, StreamPack,
+};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use serde_json::{Number, Value};
+use serde_json::{json, Number, Value};
#[derive(Clone, Copy, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
@@ -137,8 +139,25 @@ pub struct FibonacciStream {
}
pub fn register(registry: &mut DiagramElementRegistry) {
+ let add_description = "Add together any set of numbers passed as input and \
+ then add the value in the config. If only one number is passed in as \
+ input, it will be added to the value set in the config.";
+ let add_examples = [
+ ConfigExample::new(
+ "Simply sum the set of numbers passed as input.",
+ json!(null),
+ ),
+ ConfigExample::new(
+ "Sum the set of numbers passed as input, and then add 5.",
+ json!(5.0),
+ ),
+ ];
+
registry.register_node_builder(
- NodeBuilderOptions::new("add").with_default_display_text("Add"),
+ NodeBuilderOptions::new("add")
+ .with_default_display_text("Add")
+ .with_description(add_description)
+ .with_examples_configs(add_examples),
|builder, config: Option| {
builder.create_map_block(move |req: JsonMessage| {
let input = match req {
@@ -162,8 +181,23 @@ pub fn register(registry: &mut DiagramElementRegistry) {
},
);
+ let sub_description = "Subtract some numbers. If an array of numbers is \
+ passed as input then the first number will be subtracted by every \
+ subsequent number. If a number is set in the config, that will also be \
+ subtracted from the output.";
+ let sub_examples = [
+ ConfigExample::new(
+ "Simply subtract the first array element by all subsequent elements.",
+ json!(null),
+ ),
+ ConfigExample::new("Additionally subtract 5 from the output.", json!(5.0)),
+ ];
+
registry.register_node_builder(
- NodeBuilderOptions::new("sub").with_default_display_text("Subtract"),
+ NodeBuilderOptions::new("sub")
+ .with_default_display_text("Subtract")
+ .with_description(sub_description)
+ .with_examples_configs(sub_examples),
|builder, config: Option| {
builder.create_map_block(move |req: JsonMessage| {
let input = match req {
@@ -187,8 +221,20 @@ pub fn register(registry: &mut DiagramElementRegistry) {
},
);
+ let mul_description = "Multiply some numbers. If an array of numbers is \
+ passed as input then all the numbers will be multiplied together. If \
+ a number is set in the config, that will also be multipled into the \
+ output.";
+ let mul_examples = [
+ ConfigExample::new("Simply multiply the input numbers together.", json!(null)),
+ ConfigExample::new("Additionally multiple the output by 5.", json!(5.0)),
+ ];
+
registry.register_node_builder(
- NodeBuilderOptions::new("mul").with_default_display_text("Multiply"),
+ NodeBuilderOptions::new("mul")
+ .with_default_display_text("Multiply")
+ .with_description(mul_description)
+ .with_examples_configs(mul_examples),
|builder, config: Option| {
builder.create_map_block(move |req: JsonMessage| {
let input = match req {
@@ -212,8 +258,23 @@ pub fn register(registry: &mut DiagramElementRegistry) {
},
);
+ let div_description = "Divide some numbers. If an array of numbers is \
+ passed as input then the first number will be divided by all \
+ subsequent numbers. If a number is set in the config, the final output \
+ will also be divided by that value.";
+ let div_examples = [
+ ConfigExample::new(
+ "Simply divide the first array element by all subsequent elements.",
+ json!(null),
+ ),
+ ConfigExample::new("Additionally divide the output by 2.", json!(2.0)),
+ ];
+
registry.register_node_builder(
- NodeBuilderOptions::new("div").with_default_display_text("Divide"),
+ NodeBuilderOptions::new("div")
+ .with_default_display_text("Divide")
+ .with_description(div_description)
+ .with_examples_configs(div_examples),
|builder, config: Option| {
builder.create_map_block(move |req: JsonMessage| {
let input = match req {
@@ -237,8 +298,27 @@ pub fn register(registry: &mut DiagramElementRegistry) {
},
);
+ let fibonacci_description = "Stream out a Fibonacci sequence. If a number \
+ is given in the config, that will be used as the order of the \
+ sequence. If no config is given then the input value will be \
+ interpreted as a number and used as the order of the sequence. If no \
+ suitable number can be found for the order then this will return an Err
+ containing the input message.";
+ let fibonacci_examples = [
+ ConfigExample::new(
+ "Generate a Fibonacci sequence whose order is the input value. If \
+ the input message cannot be interpreted as a number then this node \
+ will return an Err.",
+ json!(null),
+ ),
+ ConfigExample::new("Generate a Fibonacci sequence of order 10.", json!(10.0)),
+ ];
+
registry.register_node_builder(
- NodeBuilderOptions::new("fibonacci").with_default_display_text("Fibonacci"),
+ NodeBuilderOptions::new("fibonacci")
+ .with_default_display_text("Fibonacci")
+ .with_description(fibonacci_description)
+ .with_examples_configs(fibonacci_examples),
|builder, config: Option| {
builder.create_map(
move |input: AsyncMap| async move {
@@ -266,12 +346,26 @@ pub fn register(registry: &mut DiagramElementRegistry) {
},
);
+ let print_description = "Prints the input to stdout. An optional string \
+ can be provided in the config to label the output.";
+
+ let print_examples = [
+ ConfigExample::new("Print the input as-is", json!(null)),
+ ConfigExample::new(
+ "Add \"printed from node: \" to the printed message",
+ json!("printed from node"),
+ ),
+ ];
+
registry
.opt_out()
.no_serializing()
.no_deserializing()
.register_node_builder(
- NodeBuilderOptions::new("print").with_default_display_text("Print"),
+ NodeBuilderOptions::new("print")
+ .with_default_display_text("Print")
+ .with_description(print_description)
+ .with_examples_configs(print_examples),
|builder, config: Option| {
let header = config.clone();
builder.create_map_block(move |request: JsonMessage| {
@@ -288,9 +382,40 @@ pub fn register(registry: &mut DiagramElementRegistry) {
)
.with_deserialize_request();
+ let less_than_description = "Compares for a less-than relationship, \
+ returning a Result based on the evaluation. Inputs can be an \
+ array of numbers or a single number value. The exact behavior will \
+ depend on the config (see examples).";
+
+ let less_than_examples = [
+ ConfigExample::new(
+ "Verify that every element in the input array is less than the next one.",
+ ComparisonConfig::None,
+ ),
+ ConfigExample::new(
+ "Verify that every element in the input array is less than OR EQUAL to the next one.",
+ ComparisonConfig::OrEqual(OrEqualTag::OrEqual),
+ ),
+ ConfigExample::new(
+ "Verify that every element in the input array is less than 10.",
+ ComparisonConfig::ComparedTo(10.0),
+ ),
+ ConfigExample::new(
+ "Verify that every element in the input array is less than or \
+ equal to 10.",
+ ComparisonConfig::Settings(ComparisonSettings {
+ compared_to: Some(10.0),
+ or_equal: true,
+ }),
+ ),
+ ];
+
registry
.register_node_builder(
- NodeBuilderOptions::new("less_than").with_default_display_text("Less Than"),
+ NodeBuilderOptions::new("less_than")
+ .with_default_display_text("Less Than")
+ .with_description(less_than_description)
+ .with_examples_configs(less_than_examples),
|builder, config: ComparisonConfig| {
let settings: ComparisonSettings = config.into();
builder.create_map_block(move |request: JsonMessage| {
@@ -300,9 +425,40 @@ pub fn register(registry: &mut DiagramElementRegistry) {
)
.with_fork_result();
+ let greater_than_description = "Compares for a greater-than relationship, \
+ returning a Result based on the evaluation. Inputs can be an \
+ array of numbers or a single number value. The exact behavior will \
+ depend on the config (see examples).";
+
+ let greater_than_examples = [
+ ConfigExample::new(
+ "Verify that every element in the input array is greater than the next one.",
+ ComparisonConfig::None,
+ ),
+ ConfigExample::new(
+ "Verify that every element in the input array is greater than OR EQUAL to the next one.",
+ ComparisonConfig::OrEqual(OrEqualTag::OrEqual),
+ ),
+ ConfigExample::new(
+ "Verify that every element in the input array is greater than 10.",
+ ComparisonConfig::ComparedTo(10.0),
+ ),
+ ConfigExample::new(
+ "Verify that every element in the input array is greater than or \
+ equal to 10.",
+ ComparisonConfig::Settings(ComparisonSettings {
+ compared_to: Some(10.0),
+ or_equal: true,
+ }),
+ ),
+ ];
+
registry
.register_node_builder(
- NodeBuilderOptions::new("greater_than").with_default_display_text("Greater Than"),
+ NodeBuilderOptions::new("greater_than")
+ .with_default_display_text("Greater Than")
+ .with_description(greater_than_description)
+ .with_examples_configs(greater_than_examples),
|builder, config: ComparisonConfig| {
let settings: ComparisonSettings = config.into();
builder.create_map_block(move |request: JsonMessage| {
diff --git a/registry.schema.json b/registry.schema.json
index feb15aa4..34a96bc5 100644
--- a/registry.schema.json
+++ b/registry.schema.json
@@ -37,6 +37,22 @@
"schemas"
],
"$defs": {
+ "ConfigExample": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "description": "A description of what this config is for",
+ "type": "string"
+ },
+ "config": {
+ "description": "The value of the config"
+ }
+ },
+ "required": [
+ "description",
+ "config"
+ ]
+ },
"MessageOperation": {
"type": "object",
"properties": {
@@ -115,6 +131,12 @@
"NodeRegistration": {
"type": "object",
"properties": {
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"config_schema": {
"$ref": "#/$defs/Schema"
},
@@ -122,6 +144,12 @@
"description": "If the user does not specify a default display text, the node ID will\n be used here.",
"type": "string"
},
+ "example_configs": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/ConfigExample"
+ }
+ },
"request": {
"type": "string"
},
@@ -140,7 +168,8 @@
"request",
"response",
"streams",
- "config_schema"
+ "config_schema",
+ "example_configs"
]
},
"Schema": {
@@ -213,12 +242,24 @@
"SectionRegistration": {
"type": "object",
"properties": {
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"config_schema": {
"$ref": "#/$defs/Schema"
},
"default_display_text": {
"type": "string"
},
+ "example_configs": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/ConfigExample"
+ }
+ },
"metadata": {
"$ref": "#/$defs/SectionMetadata"
}
@@ -226,7 +267,8 @@
"required": [
"default_display_text",
"metadata",
- "config_schema"
+ "config_schema",
+ "example_configs"
]
}
}
diff --git a/src/diagram/registration.rs b/src/diagram/registration.rs
index e13e4b57..64574b91 100644
--- a/src/diagram/registration.rs
+++ b/src/diagram/registration.rs
@@ -61,6 +61,8 @@ pub struct NodeRegistration {
pub(super) response: TypeInfo,
pub(super) streams: HashMap, TypeInfo>,
pub(super) config_schema: Schema,
+ pub(super) description: Option,
+ pub(super) example_configs: Vec,
/// Creates an instance of the registered node.
#[serde(skip)]
@@ -243,6 +245,8 @@ impl<'a, DeserializeImpl, SerializeImpl, Cloneable>
Ok(node.into())
})),
+ description: options.description,
+ example_configs: options.examples_configs,
};
self.registry.nodes.insert(options.id.clone(), registration);
@@ -650,6 +654,8 @@ pub struct SectionRegistration {
pub(super) default_display_text: DisplayText,
pub(super) metadata: SectionMetadata,
pub(super) config_schema: Schema,
+ pub(super) description: Option,
+ pub(super) example_configs: Vec,
#[serde(skip)]
create_section_impl: RefCell>,
@@ -673,7 +679,7 @@ where
{
fn into_section_registration(
self,
- name: BuilderId,
+ options: &SectionBuilderOptions,
schema_generator: &mut SchemaGenerator,
) -> SectionRegistration;
}
@@ -686,17 +692,23 @@ where
{
fn into_section_registration(
mut self,
- name: BuilderId,
+ options: &SectionBuilderOptions,
schema_generator: &mut SchemaGenerator,
) -> SectionRegistration {
SectionRegistration {
- default_display_text: name,
+ default_display_text: options
+ .default_display_text
+ .as_ref()
+ .unwrap_or(&options.id)
+ .clone(),
metadata: SectionT::metadata().clone(),
config_schema: schema_generator.subschema_for::<()>(),
create_section_impl: RefCell::new(Box::new(move |builder, config| {
let section = self(builder, serde_json::from_value::(config).unwrap());
Box::new(section)
})),
+ description: options.description.clone(),
+ example_configs: options.example_configs.clone(),
}
}
}
@@ -1461,12 +1473,8 @@ impl DiagramElementRegistry {
SectionBuilder: IntoSectionRegistration,
SectionT: Section,
{
- let reg = section_builder.into_section_registration(
- options
- .default_display_text
- .unwrap_or_else(|| options.id.clone()),
- &mut self.messages.schema_generator,
- );
+ let reg = section_builder
+ .into_section_registration(&options, &mut self.messages.schema_generator);
self.sections.insert(options.id, reg);
SectionT::on_register(self);
}
@@ -1587,6 +1595,7 @@ impl DiagramElementRegistry {
}
}
+#[derive(Clone)]
#[non_exhaustive]
pub struct NodeBuilderOptions {
/// The unique identifier for this node builder. Diagrams will use this ID
@@ -1595,6 +1604,37 @@ pub struct NodeBuilderOptions {
/// If this is not specified, the id field will be used as the default
/// display text.
pub default_display_text: Option,
+ /// Optional text to describe the builder.
+ pub description: Option,
+ /// Examples of configurations that can be used with this node builder.
+ pub examples_configs: Vec,
+}
+
+#[derive(Clone, Serialize, JsonSchema)]
+pub struct ConfigExample {
+ /// A description of what this config is for
+ pub description: String,
+ /// The value of the config
+ pub config: JsonMessage,
+}
+
+impl ConfigExample {
+ /// Create a new config example.
+ ///
+ /// Note that this function will panic if the `config` argument fails to be
+ /// serialized into a [`JsonMessage`], which happens if the data structure
+ /// contains a map with non-string keys or its [`Serialize`] implementation
+ /// produces an error. It's recommended to only use this during application
+ /// startup to avoid runtime failures.
+ ///
+ /// To construct a [`ConfigExample`] with no risk of panicking, you can
+ /// directly use normal structure initialization.
+ pub fn new(description: impl ToString, config: impl Serialize) -> Self {
+ Self {
+ description: description.to_string(),
+ config: serde_json::to_value(config).expect("failed to serialize example config"),
+ }
+ }
}
impl NodeBuilderOptions {
@@ -1602,6 +1642,8 @@ impl NodeBuilderOptions {
Self {
id: id.into(),
default_display_text: None,
+ description: None,
+ examples_configs: Default::default(),
}
}
@@ -1609,6 +1651,19 @@ impl NodeBuilderOptions {
self.default_display_text = Some(text.into());
self
}
+
+ pub fn with_description(mut self, text: impl Into) -> Self {
+ self.description = Some(text.into());
+ self
+ }
+
+ pub fn with_examples_configs(
+ mut self,
+ example_configs: impl IntoIterator- ,
+ ) -> Self {
+ self.examples_configs = example_configs.into_iter().collect();
+ self
+ }
}
#[non_exhaustive]
@@ -1619,6 +1674,10 @@ pub struct SectionBuilderOptions {
/// If this is not specified, the id field will be used as the default
/// display text.
pub default_display_text: Option,
+ /// Optional text to describe the builder.
+ pub description: Option,
+ /// Examples of configurations that can be used with this section builder.
+ pub example_configs: Vec,
}
impl SectionBuilderOptions {
@@ -1626,6 +1685,8 @@ impl SectionBuilderOptions {
Self {
id: id.into(),
default_display_text: None,
+ description: None,
+ example_configs: Default::default(),
}
}
@@ -1633,6 +1694,19 @@ impl SectionBuilderOptions {
self.default_display_text = Some(text.into());
self
}
+
+ pub fn with_description(mut self, text: impl Into) -> Self {
+ self.description = Some(text.into());
+ self
+ }
+
+ pub fn with_example_configs(
+ mut self,
+ example_configs: impl IntoIterator
- ,
+ ) -> Self {
+ self.example_configs = example_configs.into_iter().collect();
+ self
+ }
}
#[cfg(test)]