import React from "react"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { computed, IObservableValue, observable, runInAction, action, makeObservable } from "mobx"; import { Panel } from "project-editor/ui-components/Panel"; import { IColumn, ITreeNode, TreeTable } from "eez-studio-ui/tree-table"; import type { IDataContext } from "project-editor/flow/flow-interfaces"; import type { Variable } from "project-editor/features/variable/variable"; import { getArrayElementTypeFromType, getObjectVariableTypeFromType, getStructureFromType } from "project-editor/features/variable/value-type"; import { FlowTabState } from "project-editor/flow/flow-tab-state"; import { ConnectionLine } from "project-editor/flow/connection-line"; import type { ComponentState, RuntimeBase } from "project-editor/flow/runtime/runtime"; import { getInputDisplayName } from "project-editor/flow/helper"; import { evalExpressionGetValueType } from "project-editor/flow/expression"; import { IconAction } from "eez-studio-ui/action"; import { showGenericDialog } from "eez-studio-ui/generic-dialog"; import { validators } from "eez-studio-shared/validation"; import { ProjectEditor } from "project-editor/project-editor-interface"; import { getValueLabel } from "project-editor/features/variable/value-type"; import { stringCompare } from "eez-studio-shared/string"; import { isArray } from "eez-studio-shared/util"; import type { UserProperty } from "project-editor/flow/user-property"; import { IObjectVariableValueFieldDescription } from "eez-studio-types"; import { SearchInput } from "eez-studio-ui/search-input"; //////////////////////////////////////////////////////////////////////////////// export function valueToString(value: any) { if (value === undefined) { return "undefined"; } try { return JSON.stringify(value); } catch (err) { try { return value.toString(); } catch (err) { return "err!"; } } } //////////////////////////////////////////////////////////////////////////////// export const WatchPanel = observer( class WatchPanel extends React.Component<{ runtime: RuntimeBase; }> { onAddExpression = async () => { const result = await showGenericDialog({ dialogDefinition: { title: "New Watch Expression", fields: [ { name: "expression", type: "string", validators: [validators.required] } ] }, values: {}, dialogContext: this.props.runtime.projectStore.project }); runInAction(() => this.props.runtime.projectStore.uiStateStore.watchExpressions.push( result.values.expression ) ); }; onEditExpression = async () => { const i = this.selectedExpression.get(); if (i == -1) { return; } const result = await showGenericDialog({ dialogDefinition: { title: "Edit Watch Expression", fields: [ { name: "expression", type: "string", validators: [validators.required] } ] }, values: { expression: this.props.runtime.projectStore.uiStateStore .watchExpressions[i] }, dialogContext: this.props.runtime.projectStore.project }); runInAction( () => (this.props.runtime.projectStore.uiStateStore.watchExpressions[ i ] = result.values.expression) ); }; onDeleteEpression = action(() => { const i = this.selectedExpression.get(); if (i == -1) { return; } runInAction(() => this.props.runtime.projectStore.uiStateStore.watchExpressions.splice( i, 1 ) ); }); selectedExpression = observable.box(-1); render() { return (
, , ]} body={ } />
); } } ); const WatchTable = observer( class WatchTable extends React.Component<{ runtime: RuntimeBase; selectedExpression: IObservableValue; }> { constructor(props: { runtime: RuntimeBase; selectedExpression: IObservableValue; }) { super(props); makeObservable(this, { searchText: observable, columns: computed, watchExpressions: computed, globalVariables: computed, localVariables: computed, selectedComponent: computed, componentInputs: computed, rootNode: computed, expandedMap: observable }); } expandedMap = new Map(); searchText = ""; onSearchChange = (event: any) => { this.searchText = event.target.value; }; filterBySearchText(text: string) { const searchText = this.searchText.trim().toLowerCase(); if (!searchText) { return true; } return text.toLowerCase().indexOf(searchText) != -1; } expanded(id: string, defaultValue: boolean) { const expandedMap = this.expandedMap; return { get() { const expanded = expandedMap.get(id); if (expanded != undefined) { return expanded; } return defaultValue; }, set(value: boolean) { expandedMap.set(id, value); } }; } get columns() { let result: IColumn[] = []; result.push({ name: "name", title: "Name" }); result.push({ name: "value", title: "Value" }); result.push({ name: "type", title: "Type" }); return result; } getValueChildren = computedFn( ( id: string, value: any, type: string | null ): (() => ITreeNode[]) | undefined => { if (type == "date") { return undefined; } const MAX_CHILDREN = 1000; // MAX_ARRAY_SIZE_TRANSFERRED_IN_DEBUGGER if (isArray(value) || value instanceof Uint8Array) { return () => { const elementType = type ? getArrayElementTypeFromType(type) : undefined; const children: ITreeNode[] = []; for ( let i = 0; i < Math.min(value.length, MAX_CHILDREN); i++ ) { const elementValue = value[i]; const name = `[${i}]`; const type = elementType ?? typeof elementValue; const valueLabel = ( {getValueLabel( this.props.runtime.projectStore.project, elementValue, type )} ); children.push( observable({ id: id + name, name, nameTitle: name, value: valueLabel, valueTitle: valueLabel, type: type, children: this.getValueChildren( id + name, elementValue, type ), selected: false, expanded: this.expanded(id + name, false) }) ); } return children; }; } if (value != null && typeof value == "object") { return () => { let structure; if (type) { const objectVariableType = getObjectVariableTypeFromType( this.props.runtime.projectStore, type ); if (objectVariableType) { value = objectVariableType.getValue ? objectVariableType.getValue(value) : undefined; const getChildren = ( value: any, valueFieldDescriptions: IObjectVariableValueFieldDescription[] ): ITreeNode[] => { const children: ITreeNode[] = []; for (const valueFieldDescription of valueFieldDescriptions) { const propertyValue = valueFieldDescription.getFieldValue( value ); const name = valueFieldDescription.name; const valueType = valueFieldDescription.valueType; const valueLabel = ( {getValueLabel( this.props.runtime .projectStore.project, propertyValue, typeof valueType == "string" ? valueType : null )} ); children.push( observable({ id: id + name, name, nameTitle: name, value: valueLabel, valueTitle: valueLabel, type: typeof valueType == "string" ? valueType : "object", children: typeof valueType == "string" ? undefined : () => getChildren( propertyValue, valueType ), selected: false, expanded: this.expanded( id + name, false ) }) ); } return children; }; return getChildren( value, objectVariableType.valueFieldDescriptions ); } structure = getStructureFromType( this.props.runtime.projectStore.project, type ); } let numChildren = 0; const children: ITreeNode[] = []; for (const name in value) { const propertyValue = value[name]; let fieldType: string | null = null; if (structure) { const field = structure.fieldsMap.get(name); if (field) { fieldType = field.type; } } const valueLabel = ( {getValueLabel( this.props.runtime.projectStore.project, propertyValue, fieldType )} ); children.push( observable({ id: id + name, name, nameTitle: name, value: valueLabel, valueTitle: valueLabel, type: fieldType ?? typeof propertyValue, children: this.getValueChildren( id + name, propertyValue, fieldType ), selected: false, expanded: this.expanded(id + name, false) }) ); if (++numChildren == MAX_CHILDREN) { break; } } return children; }; } return undefined; } ); get watchExpressions() { const result = this.selectedComponent; return observable({ id: "expressions", name: "Expressions", value: undefined, type: "", children: () => this.props.runtime.projectStore.uiStateStore.watchExpressions .filter(expression => this.filterBySearchText(expression) ) .map((expression, i) => { let watchExpressionLabel; let value; let type: any; let className: string | undefined; if (result) { try { ({ value, valueType: type } = evalExpressionGetValueType( result.flowState, result.component, expression )); watchExpressionLabel = ( {getValueLabel( this.props.runtime.projectStore .project, value, type )} ); } catch (err) { watchExpressionLabel = err.toString(); type = ""; className = "error"; } } else { watchExpressionLabel = "undefined"; type = "undefined"; } return observable({ id: "expressions-" + expression, name: expression, nameTitle: expression, value: watchExpressionLabel, valueTitle: watchExpressionLabel, type, children: this.getValueChildren( "expressions-" + expression, value, type ), selected: this.props.selectedExpression.get() == i, expanded: this.expanded( "expressions-" + expression, false ), className, data: i }); }), selected: false, expanded: this.expanded("expressions", true) }); } getVariableTreeNodes = ( id: string, variables: (Variable | UserProperty)[] ) => { variables = variables.slice(); variables.sort((a, b) => stringCompare(a.fullName, b.fullName)); return variables .filter(variable => this.filterBySearchText(variable.fullName)) .map(variable => { const flowState = this.props.runtime.selectedFlowState; let dataContext: IDataContext; if (flowState) { dataContext = flowState.dataContext; } else { dataContext = this.props.runtime.projectStore.dataContext; } const name = variable.fullName; const value = dataContext.get(name); const valueLabel = ( {getValueLabel( this.props.runtime.projectStore.project, value, variable.type )} ); return observable({ id: id + name, name: name, nameTitle: name, value: valueLabel, valueTitle: valueLabel, type: variable.type, children: this.getValueChildren( id + name, value, variable.type ), selected: false, expanded: this.expanded(id + name, false) }); }); }; get globalVariables() { return observable({ id: "global-variables", name: "Global variables", value: undefined, type: "", children: () => this.getVariableTreeNodes( "global-variables", this.props.runtime.projectStore.project .allGlobalVariables ), selected: false, expanded: this.expanded("global-variables", true) }); } get localVariables() { const flowState = this.props.runtime.selectedFlowState; if (!flowState) { return undefined; } if (flowState.flow.userPropertiesAndLocalVariables.length == 0) { return undefined; } return observable({ id: "local-variables", name: "Local variables", value: undefined, type: "", children: () => this.getVariableTreeNodes( "local-variables", flowState.flow.userPropertiesAndLocalVariables ), selected: false, expanded: this.expanded("local-variables", true) }); } getComponentStateInputsTreeNodes = ( id: string, componentState: ComponentState ) => { const inputs = componentState.component.inputs.filter( input => input.name != "@seqin" ); return inputs.map(input => { const name = getInputDisplayName( componentState.component, input.name ); let value = componentState.getInputValue(input.name); let valueLabel = ( {getValueLabel( this.props.runtime.projectStore.project, value, null )} ); return observable({ id: id + input.name, name: name, nameTitle: name, value: valueLabel, valueTitle: valueLabel, type: input.type, children: this.getValueChildren( id + input.name, value, null ), selected: false, expanded: this.expanded(id + input.name, false) }); }); }; get selectedComponent() { const flowState = this.props.runtime.selectedFlowState; if (!flowState) { return undefined; } const editorState = this.props.runtime.projectStore.editorsStore.activeEditor ?.state; if (!(editorState instanceof FlowTabState)) { return undefined; } const selectedObjects = editorState.selectedObjects; let component; if (selectedObjects.length == 1) { component = selectedObjects[0]; } else if (selectedObjects.length == 3) { const connectionLines = selectedObjects.filter( selectedObject => selectedObject instanceof ConnectionLine ) as ConnectionLine[]; if (connectionLines.length == 1) { component = connectionLines[0].targetComponent; } } if (!component) { return undefined; } if (!(component instanceof ProjectEditor.ComponentClass)) { return undefined; } return { flowState, component }; } get componentInputs() { const result = this.selectedComponent; if (!result) { return undefined; } const { flowState, component } = result; const inputs = component.inputs.filter( input => input.name != "@seqin" && this.filterBySearchText(input.name) ); if (inputs.length == 0) { return undefined; } // if (flowState.flow.components.indexOf(component) == -1) { // return undefined; // } const componentState = flowState.getComponentState(component); return observable({ id: "component-inputs", name: "Component inputs", value: undefined, type: "", children: () => this.getComponentStateInputsTreeNodes( "component-inputs", componentState ), selected: false, expanded: this.expanded("component-inputs", true) }); } get rootNode(): ITreeNode { const treeNode: ITreeNode = { id: "root", label: "", children: () => { const children: ITreeNode[] = []; if ( this.props.runtime.projectStore.uiStateStore .watchExpressions.length > 0 ) { children.push(this.watchExpressions); } children.push(this.globalVariables); const localVariables = this.localVariables; if (localVariables) { children.push(localVariables); } const componentInputs = this.componentInputs; if (componentInputs) { children.push(componentInputs); } return children; }, selected: false, expanded: this.expanded("root", true) }; return treeNode; } selectedNode: ITreeNode | undefined; selectNode = action((node?: ITreeNode) => { if (this.selectedNode) { this.selectedNode.selected = false; this.selectedNode = undefined; } if (node && typeof node.data == "number") { this.props.selectedExpression.set(node.data); this.selectedNode = node; node.selected = true; } else { this.props.selectedExpression.set(-1); } }); render() { return (
{ this.searchText = ""; })} onChange={this.onSearchChange} onKeyDown={this.onSearchChange} >
{ }
); } } ); ////////////////////////////////////////////////////////////////////////////////