diff --git a/diagram-editor/dist.tar.gz b/diagram-editor/dist.tar.gz index 715730fd..9872b933 100644 Binary files a/diagram-editor/dist.tar.gz and b/diagram-editor/dist.tar.gz differ diff --git a/diagram-editor/frontend/api.preprocessed.schema.json b/diagram-editor/frontend/api.preprocessed.schema.json index 497e3424..56ccc60f 100644 --- a/diagram-editor/frontend/api.preprocessed.schema.json +++ b/diagram-editor/frontend/api.preprocessed.schema.json @@ -236,6 +236,17 @@ "default_trace": { "$ref": "#/$defs/TraceToggle" }, + "description": { + "description": "Optional text to describe the workflow.", + "type": "string" + }, + "example_inputs": { + "description": "Examples of inputs that can be used with this workflow.", + "items": { + "$ref": "#/$defs/ExampleInput" + }, + "type": "array" + }, "extensions": { "additionalProperties": true, "default": {}, @@ -569,6 +580,19 @@ } ] }, + "ExampleInput": { + "properties": { + "description": { + "type": "string" + }, + "value": true + }, + "required": [ + "value", + "description" + ], + "type": "object" + }, "ForkCloneSchema": { "description": "If the request is cloneable, clone it into multiple responses that can\n each be sent to a different operation. The `next` property is an array.\n\n This creates multiple simultaneous branches of execution within the\n workflow. Usually when you have multiple branches you will either\n * race - connect all branches to `terminate` and the first branch to\n finish \"wins\" the race and gets to the be output\n * join - connect each branch into a buffer and then use the `join`\n operation to reunite them\n * collect - TODO(@mxgrey): [add the collect operation](https://github.com/open-rmf/crossflow/issues/59)\n\n # Examples\n ```\n # crossflow::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"begin_race\",\n \"ops\": {\n \"begin_race\": {\n \"type\": \"fork_clone\",\n \"next\": [\n \"ferrari\",\n \"mustang\"\n ]\n },\n \"ferrari\": {\n \"type\": \"node\",\n \"builder\": \"drive\",\n \"config\": \"ferrari\",\n \"next\": { \"builtin\": \"terminate\" }\n },\n \"mustang\": {\n \"type\": \"node\",\n \"builder\": \"drive\",\n \"config\": \"mustang\",\n \"next\": { \"builtin\": \"terminate\" }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())", "properties": { diff --git a/diagram-editor/frontend/command-panel.tsx b/diagram-editor/frontend/command-panel.tsx index 5789ec41..452a4c9b 100644 --- a/diagram-editor/frontend/command-panel.tsx +++ b/diagram-editor/frontend/command-panel.tsx @@ -1,4 +1,4 @@ -import { Button, ButtonGroup, styled, Tooltip } from '@mui/material'; +import { Button, ButtonGroup, styled, Tooltip, useTheme } from '@mui/material'; import { type NodeChange, Panel } from '@xyflow/react'; import React from 'react'; import AutoLayoutButton from './auto-layout-button'; @@ -7,6 +7,7 @@ import { EditorMode, useEditorMode } from './editor-mode'; import type { DiagramEditorNode } from './nodes'; import { MaterialSymbol } from './nodes'; import { RunButton } from './run-button'; +import DiagramPropertiesDrawer from './diagram-properties-drawer'; export interface CommandPanelProps { onNodeChanges: (changes: NodeChange[]) => void; @@ -31,15 +32,34 @@ function CommandPanel({ onExportClick, onLoadDiagram, }: CommandPanelProps) { + const theme = useTheme(); const [openEditTemplatesDialog, setOpenEditTemplatesDialog] = React.useState(false); + const [openDiagramPropertiesDrawer, setOpenDiagramPropertiesDrawer] = + React.useState(false); const [editorMode] = useEditorMode(); return ( <> - {editorMode.mode === EditorMode.Normal && } + {editorMode.mode === EditorMode.Normal && ( + + )} + {editorMode.mode === EditorMode.Normal && ( + + + + )} {editorMode.mode === EditorMode.Normal && ( + + + + ); +} + +export default DiagramPropertiesDrawer; diff --git a/diagram-editor/frontend/diagram-properties-provider.tsx b/diagram-editor/frontend/diagram-properties-provider.tsx new file mode 100644 index 00000000..897b6dcc --- /dev/null +++ b/diagram-editor/frontend/diagram-properties-provider.tsx @@ -0,0 +1,37 @@ +import { createContext, PropsWithChildren, useContext, useState } from 'react'; +import type { ExampleInput } from './types/api'; + +export interface DiagramProperties { + description?: string; + example_inputs?: ExampleInput[]; +} + +export type DiagramPropertiesContext = [ + DiagramProperties, + React.Dispatch>, +]; + +const DiagramPropertiesContextComp = + createContext(null); + +export function DiagramPropertiesProvider({ children }: PropsWithChildren) { + const [diagramProperties, setDiagramProperties] = + useState({}); + + return ( + + {children} + + ); +} + +export const useDiagramProperties = (): DiagramPropertiesContext => { + const context = useContext(DiagramPropertiesContextComp); + if (!context) { + throw new Error( + 'useDiagramProperties must be used within a TemplatesProvider'); + } + return context; +}; diff --git a/diagram-editor/frontend/export-diagram-dialog.tsx b/diagram-editor/frontend/export-diagram-dialog.tsx index 764deb4f..25a49a50 100644 --- a/diagram-editor/frontend/export-diagram-dialog.tsx +++ b/diagram-editor/frontend/export-diagram-dialog.tsx @@ -19,6 +19,7 @@ import { useRegistry } from './registry-provider'; import { useTemplates } from './templates-provider'; import { useEdges } from './use-edges'; import { exportDiagram } from './utils/export-diagram'; +import { useDiagramProperties } from './diagram-properties-provider'; export interface ExportDiagramDialogProps { open: boolean; @@ -39,9 +40,10 @@ function ExportDiagramDialogInternal({ const [templates] = useTemplates(); const registry = useRegistry(); const loadContext = useLoadContext(); + const [diagramProperties] = useDiagramProperties(); const dialogDataPromise = useMemo(async () => { - const diagram = exportDiagram(registry, nodeManager, edges, templates); + const diagram = exportDiagram(registry, nodeManager, edges, templates, diagramProperties ?? {}); if (loadContext?.diagram.extensions) { diagram.extensions = loadContext.diagram.extensions; } @@ -69,7 +71,7 @@ function ExportDiagramDialogInternal({ } satisfies DialogData; return dialogData; - }, [registry, nodeManager, edges, templates, loadContext]); + }, [registry, nodeManager, edges, templates, loadContext, diagramProperties]); const dialogData = use(dialogDataPromise); diff --git a/diagram-editor/frontend/run-button.tsx b/diagram-editor/frontend/run-button.tsx index 838df011..51f645d2 100644 --- a/diagram-editor/frontend/run-button.tsx +++ b/diagram-editor/frontend/run-button.tsx @@ -11,7 +11,7 @@ import { Typography, useTheme, } from '@mui/material'; -import { useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useApiClient } from './api-client-provider'; import { useNodeManager } from './node-manager'; import { MaterialSymbol } from './nodes'; @@ -19,23 +19,31 @@ import { useRegistry } from './registry-provider'; import { useTemplates } from './templates-provider'; import { useEdges } from './use-edges'; import { exportDiagram } from './utils/export-diagram'; +import { useDiagramProperties } from './diagram-properties-provider'; type ResponseContent = { raw: string } | { err: string }; -export function RunButton() { +const DefaultResponseContent: ResponseContent = { raw: '' }; + +export interface RunButtonProps { + requestJsonString: string; +} + +export function RunButton({ requestJsonString }: RunButtonProps) { const nodeManager = useNodeManager(); const edges = useEdges(); const [openPopover, setOpenPopover] = useState(false); const buttonRef = useRef(null); const theme = useTheme(); - const [requestJson, setRequestJson] = useState(''); - const [responseContent, setResponseContent] = useState({ - raw: '', - }); + const [requestJson, setRequestJson] = useState(requestJsonString); + const [responseContent, setResponseContent] = useState( + DefaultResponseContent + ); const apiClient = useApiClient(); const [templates, _setTemplates] = useTemplates(); const registry = useRegistry(); const [running, setRunning] = useState(false); + const [diagramProperties, _] = useDiagramProperties(); const requestError = useMemo(() => { try { @@ -61,7 +69,7 @@ export function RunButton() { const handleRunClick = () => { try { const request = JSON.parse(requestJson); - const diagram = exportDiagram(registry, nodeManager, edges, templates); + const diagram = exportDiagram(registry, nodeManager, edges, templates, diagramProperties); apiClient.postRunWorkflow(diagram, request).subscribe({ next: (response) => { setResponseContent({ raw: JSON.stringify(response, null, 2) }); @@ -87,7 +95,10 @@ export function RunButton() { setOpenPopover(false)} + onClose={() => { + setOpenPopover(false); + setResponseContent(DefaultResponseContent); + }} anchorEl={buttonRef.current} anchorOrigin={{ vertical: 'bottom', @@ -141,7 +152,26 @@ export function RunButton() { error={requestError} sx={{ backgroundColor: theme.palette.background.paper }} /> - Response: + + Response: + {'err' in responseContent ? ( + + ) : 'raw' in responseContent && responseContent.raw.length > 0 ? ( + + ) : ( + <> + )} + , + diagramProperties: DiagramProperties, ): Diagram { const diagram: Diagram = { $schema: @@ -483,6 +485,8 @@ export function exportDiagram( } } + diagram.description = diagramProperties.description; + diagram.example_inputs = diagramProperties.example_inputs; return diagram; } diff --git a/diagram.schema.json b/diagram.schema.json index 01e98915..945154eb 100644 --- a/diagram.schema.json +++ b/diagram.schema.json @@ -3,10 +3,21 @@ "title": "Diagram", "type": "object", "properties": { + "description": { + "description": "Optional text to describe the workflow.", + "type": "string" + }, "default_trace": { "description": "Whether the operations in the workflow should be traced by default.\n Being traced means each operation will emit an event each time it is\n triggered. You can decide whether that event contains the serialized\n message data that triggered the operation.\n\n If crossflow is not compiled with the \"trace\" feature then this\n setting will have no effect.", "$ref": "#/$defs/TraceToggle" }, + "example_inputs": { + "description": "Examples of inputs that can be used with this workflow.", + "type": "array", + "items": { + "$ref": "#/$defs/ExampleInput" + } + }, "extensions": { "description": "Settings for each extension.", "type": "object", @@ -375,6 +386,19 @@ } ] }, + "ExampleInput": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "value": true + }, + "required": [ + "value", + "description" + ] + }, "ForkCloneSchema": { "description": "If the request is cloneable, clone it into multiple responses that can\n each be sent to a different operation. The `next` property is an array.\n\n This creates multiple simultaneous branches of execution within the\n workflow. Usually when you have multiple branches you will either\n * race - connect all branches to `terminate` and the first branch to\n finish \"wins\" the race and gets to the be output\n * join - connect each branch into a buffer and then use the `join`\n operation to reunite them\n * collect - TODO(@mxgrey): [add the collect operation](https://github.com/open-rmf/crossflow/issues/59)\n\n # Examples\n ```\n # crossflow::Diagram::from_json_str(r#\"\n {\n \"version\": \"0.1.0\",\n \"start\": \"begin_race\",\n \"ops\": {\n \"begin_race\": {\n \"type\": \"fork_clone\",\n \"next\": [\n \"ferrari\",\n \"mustang\"\n ]\n },\n \"ferrari\": {\n \"type\": \"node\",\n \"builder\": \"drive\",\n \"config\": \"ferrari\",\n \"next\": { \"builtin\": \"terminate\" }\n },\n \"mustang\": {\n \"type\": \"node\",\n \"builder\": \"drive\",\n \"config\": \"mustang\",\n \"next\": { \"builtin\": \"terminate\" }\n }\n }\n }\n # \"#)?;\n # Ok::<_, serde_json::Error>(())", "type": "object", diff --git a/examples/diagram/calculator/diagrams/clone_from_buffer.json b/examples/diagram/calculator/diagrams/clone_from_buffer.json index 58add4e0..f1c2e86c 100644 --- a/examples/diagram/calculator/diagrams/clone_from_buffer.json +++ b/examples/diagram/calculator/diagrams/clone_from_buffer.json @@ -1,6 +1,17 @@ { "$schema": "https://raw.githubusercontent.com/open-rmf/crossflow/refs/heads/main/diagram.schema.json", "version": "0.1.0", + "description": "Workflow that splits up the 2-number input list, multiplies them individually, checks if the output of the first multiplication is smaller than the output of the second, terminates if so, otherwise, multiply the second output again and repeat.", + "example_inputs": [ + { + "description": "At the first comparison, 123*100=12300 is still more than 456*10=4560, hence 4560 is multiplied again to terminate at 12300 < 45600.", + "value": "[123, 456]" + }, + { + "description": "At the first comparison, 10*100=1000 is still more than 1*10=10, hence 10 is multiplied 3 more times to terminate at 1000 < 10000.", + "value": "[10, 1]" + } + ], "templates": {}, "start": "initial_split", "ops": { diff --git a/examples/diagram/calculator/diagrams/countdown.json b/examples/diagram/calculator/diagrams/countdown.json index 7a0a53ce..38686911 100644 --- a/examples/diagram/calculator/diagrams/countdown.json +++ b/examples/diagram/calculator/diagrams/countdown.json @@ -1,6 +1,17 @@ { "$schema": "https://raw.githubusercontent.com/open-rmf/crossflow/refs/heads/main/diagram.schema.json", "version": "0.1.0", + "description": "Basic workflow that counts down from the input number by subtracting 1 each time and printing it out. The remainder will be in the response.", + "example_inputs": [ + { + "description": "Counts dwon from 123", + "value": "123" + }, + { + "description": "Counts dwon from 456", + "value": "456" + } + ], "templates": {}, "start": "comparison", "ops": { diff --git a/examples/diagram/calculator/diagrams/fibonacci.json b/examples/diagram/calculator/diagrams/fibonacci.json index ad57f861..354500bc 100644 --- a/examples/diagram/calculator/diagrams/fibonacci.json +++ b/examples/diagram/calculator/diagrams/fibonacci.json @@ -1,6 +1,17 @@ { "$schema": "https://raw.githubusercontent.com/open-rmf/crossflow/refs/heads/main/diagram.schema.json", "version": "0.1.0", + "description": "Basic workflow that prints out the fibonacci sequence of the input number.", + "example_inputs": [ + { + "description": "Prints out the fibonacci sequence of 10", + "value": "10" + }, + { + "description": "Prints out the fibonacci sequence of 20", + "value": "20" + } + ], "templates": {}, "start": "fibonacci", "ops": { diff --git a/examples/diagram/calculator/diagrams/multiply_by_3.json b/examples/diagram/calculator/diagrams/multiply_by_3.json index e3421e46..a3c8ff39 100644 --- a/examples/diagram/calculator/diagrams/multiply_by_3.json +++ b/examples/diagram/calculator/diagrams/multiply_by_3.json @@ -1,6 +1,17 @@ { "$schema": "../../../diagram.schema.json", "version": "0.1.0", + "description": "Basic workflow that multiplies the interger input by 3.", + "example_inputs": [ + { + "description": "Multiply 123 by 3 to get 369", + "value": "123" + }, + { + "description": "Multiply 456 by 3 to get 1368", + "value": "456" + } + ], "start": "mul3", "ops": { "mul3": { diff --git a/examples/diagram/calculator/diagrams/split_and_join.json b/examples/diagram/calculator/diagrams/split_and_join.json index 1777c19d..2b9c48a5 100644 --- a/examples/diagram/calculator/diagrams/split_and_join.json +++ b/examples/diagram/calculator/diagrams/split_and_join.json @@ -1,6 +1,17 @@ { "$schema": "https://raw.githubusercontent.com/open-rmf/crossflow/refs/heads/main/diagram.schema.json", "version": "0.1.0", + "description": "Workflow that splits up the 2-number input list, multiplies them individually, joins them, and adds them together.", + "example_inputs": [ + { + "description": "Multiplies 123 by 100 to get 12300, multiplies 456 by 10 to get 4560, summing up the results to get 16860", + "value": "[123, 456]" + }, + { + "description": "Multiply 10 by 100 to get 1000, multiplies -123 by 10 to get -1230, summing up the results to get -230", + "value": "[10, -123]" + } + ], "templates": {}, "start": "split", "ops": { diff --git a/src/diagram.rs b/src/diagram.rs index 5e4188f8..090d90da 100644 --- a/src/diagram.rs +++ b/src/diagram.rs @@ -367,6 +367,12 @@ pub struct ExtensionSettings { pub extensions: HashMap, } +#[derive(Default, Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize)] +pub struct ExampleInput { + pub value: JsonMessage, + pub description: String, +} + #[derive(Debug, Clone, JsonSchema, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct Diagram { @@ -409,6 +415,14 @@ pub struct Diagram { #[serde(flatten)] pub extensions: Option, + + /// Optional text to describe the workflow. + #[serde(default, skip_serializing_if = "is_default")] + pub description: String, + + /// Examples of inputs that can be used with this workflow. + #[serde(default, skip_serializing_if = "is_default")] + example_inputs: Vec, } #[derive(Default, Debug, Clone, Copy, JsonSchema, Serialize, Deserialize, PartialEq, Eq)] @@ -469,6 +483,8 @@ impl Diagram { ops: Default::default(), default_trace: Default::default(), extensions: None, + description: Default::default(), + example_inputs: Default::default(), } }