-
Notifications
You must be signed in to change notification settings - Fork 258
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
Question: how to use initial value as default value, and can be overwrite by IP passed from connected component. #1049
Comments
Summerize: I'm using a helper import ProcessInput from 'noflo/lib/ProcessInput';
/**
* Treat values set in the NodeDetailPanel UI as default value.
*
* So we need a way in the component to know if value is passed from connected component or from initial packet, if it is from connected component then it won't be replayed, if it is from initial packet, then it is saved and used as default value, which can be overwrite by IP passed from connected component.
*/
export function getDataOrDefault<T>(key: string, input: ProcessInput, defaultValues: Record<string, unknown> = {}): T | undefined {
const ip = input.get(key);
if (ip === null || ip === undefined) {
return defaultValues[key] as T;
}
if (Array.isArray(ip)) {
// TODO: handle array, why will this be an array of IP?
return defaultValues[key] as T;
}
if (ip.initial) {
defaultValues[key] = ip.data;
return defaultValues[key] as T;
}
return ip.data as T ?? defaultValues[key] as T;
} and in the component /* eslint-disable @typescript-eslint/strict-boolean-expressions */
/* eslint-disable unicorn/no-null, @typescript-eslint/require-await */
// Load the NoFlo interface
import { getDataOrDefault } from '@/pages/Workflow/GraphEditor/utils/getDataOrDefault';
import { Component } from 'noflo';
import type { ITextFieldProps, UIEffectsContext } from '../types/UIEffectsContext';
export const getComponent = () => new TextField();
class TextField extends Component {
description = 'Wait for user input some text.';
icon = 'commenting-o';
uiElementID?: string;
uiEffects?: UIEffectsContext;
defaultValues: Record<string, string> = {};
constructor() {
super();
// Define the component's inports
this.inPorts.add('control', {
datatype: 'bang',
description: 'Trigger the input box to show.',
});
this.inPorts.add('label', {
datatype: 'string',
description: 'Name of this input, should be a short text.',
required: true,
});
this.inPorts.add('desc', {
datatype: 'string',
description: 'Description text show on the input box. Can be a one line long text.',
});
this.inPorts.add('intro', {
datatype: 'string',
description: 'Introduction text show before the input box. Can be a multiple line long text.',
});
this.inPorts.add('placeholder', {
datatype: 'string',
description: 'Text to display when the input box is empty.',
});
this.inPorts.add('ui_effects', {
datatype: 'object',
description: 'Used by system, inject UI related methods.',
required: true,
});
this.outPorts.add('out', {
datatype: 'string',
});
// Register a process handler for incoming data
this.process((input, output, context) => {
this.uiEffects ??= input.getData('ui_effects') as UIEffectsContext | undefined;
if (this.uiEffects === undefined) {
this.deactivate(context);
return;
}
// If 'in' port is not triggered, return
if (!input.hasData('control')) {
this.deactivate(context);
return;
}
const control = input.getData('control') as undefined | null | true;
// rapidly receive null here, still stuck
if (control !== true) {
this.deactivate(context);
return;
}
const props: ITextFieldProps = {
label: getDataOrDefault('label', input, this.defaultValues),
description: getDataOrDefault('desc', input, this.defaultValues),
introduction: getDataOrDefault('intro', input, this.defaultValues),
placeholder: getDataOrDefault('placeholder', input, this.defaultValues),
};
// If we already have an UI element created, update it. Otherwise, create a new one.
if (this.uiElementID === undefined) {
this.uiElementID = this.uiEffects.addElement({ type: 'textField', props });
// wait for result, and sent to outPort
// TODO: change to async await when https://github.com/noflo/noflo/issues/1047 is fixed.
void this.uiEffects.onSubmit(this.uiElementID).then(resultText => {
this.uiElementID = undefined;
output.sendDone({ out: resultText });
});
} else {
this.uiEffects.updateElementProps({ id: this.uiElementID, props });
this.deactivate(context);
}
});
}
async tearDown() {
if (this.uiElementID !== undefined) {
// set to submit state
this.uiEffects?.submitElement?.(this.uiElementID, null);
}
}
} |
InPorts can have a default out of the box, just use the default key |
That's different, So I still need the solution described above. |
On graph level you'd use an IIP. If you also export the inport, then consumers of your graph have an opportunity to override. |
Replay Initial IPs
Human:
This is a noflo graph with three nodes, and I've add several initialvalue to the TextField node:
Here are source typescript code for two custom nodes:
I've connect TextField node to ResultText and then back to TextField node, so it works like a REPL. In first execution, the TextField renders UI with props, where I construct props using initial IPs (contains data like
Label
andDesc
), but when it loop back to TextField node from ResultText, the initial value are used, so this time, props will be empty. But I hope I can always apply same initial value for selected ports (maybe add an option to "replay" them?), how to do this in noflo?GPT4:
In NoFlo, the initial packets (IPs) are usually sent only once during the graph's startup. This means when you loop from the
ResultText
node back to theTextField
node, the initial IPs aren't sent again, leading to the observed behavior.To achieve the behavior you want, where the initial values are applied every time, you need a mechanism that can "store and replay" those initial IPs.
Here's a suggested approach:
Use a State Node: Create a custom stateful component that receives the initial IPs, stores them, and then sends them out whenever it receives a bang from the
ResultText
node. This way, you effectively "replay" the initial IPs after every cycle.Graph Modifications: Connect this state node between the
ResultText
node and theTextField
node. The sequence would be:TextField
->ResultText
ResultText
->StateNode
(on receiving a bang)StateNode
->TextField
(sends the initial IPs)State Node Implementation:
Here's a very basic TypeScript example for this
StateNode
:Now, modify your graph to integrate this new
StateNode
in the loop, and it should replay the initial IPs on every cycle.Human:
This is the react component I'm using to edit the initial value:
Maybe in the form that edit initial value for inPorts, for each field, I can add a check box "always set this value", if checked, I add metadata in the IP to inform the component to save the data, and reuse the data in future execution of process method, if it gets undefined from that inPort. Is this a good solution? Is there better solution?
GPT4:
From your description, you want to add a checkbox to each input field in the form, which, when checked, saves the data for reuse in future executions. This approach provides user flexibility to decide which data is essential for them, but there are some considerations to make.
User Experience and Complexity: Adding a checkbox to every input might complicate the user interface. Users may need to spend extra time understanding the functionality and implications of these checkboxes.
Data Integrity and Consistency: By allowing users to save specific data for reuse, you're assuming that the data will remain valid or relevant in the future. This may not always be the case, and could lead to unexpected outcomes if the data becomes outdated or incorrect.
Data Storage: Remember that storing more data can increase memory consumption, especially if there are many of these nodes with many saved initial values.
Node Mutability: If a node's behavior or its expected inputs change in the future, these saved initial values could become irrelevant or even detrimental.
Given these considerations, here are a couple of alternative solutions:
Option A: Default Values in Schemas
Instead of relying on user-set checkboxes, you can define default values in your JSON Schema. If an input field is not provided a value, the default will be used. This is a common pattern and ensures a fallback for unspecified fields.
However, this won't be dynamic and won't adjust based on user past behavior.
Option B: Local Storage / Session Storage
Another approach is to save user inputs in local or session storage, so when they revisit the form, their previous inputs are pre-filled. This way, you're not altering the node's behavior based on past executions but providing a more user-friendly experience.
Option C: "Remember my choice" Option
Instead of a checkbox for every input field, have a single checkbox or toggle button for the whole form that says "Remember these values". When checked, you can store the form data, and when this node is revisited, you can pre-fill the values.
If you decide to stick with your current approach, consider providing tooltips or help messages next to the checkboxes, explaining their purpose. Moreover, consider implementing a mechanism to review or clear these saved values.
Regardless of the solution, remember to inform the user adequately about the behavior of these checkboxes or saved states to avoid any confusion or unexpected behavior.
Human:
I will use Option A: Default Values, but I will treat values set in the NodeDetailPanel UI as default value, So I need a way in the component to know if value is passed from connected component or from initial packet, if it is from connected component then it won't be replayed, if it is from initial packet, then it is saved and used as default value, which can be overwrite by IP passed from connected component.
GPT4:
Great choice! Option A offers a good balance between flexibility and simplicity.
Given your needs, you can use the following approach:
Distinguish Source of Data: The key requirement here is the ability to differentiate between an IP (initial packet) and data passed from a connected component.
source
metadata, you can decide whether to use the value as a default or not.Store the Initial Value:
Whenever you receive an IP, update a stored 'default value' for the component in question.
This stored default value is only replaced if you receive another IP in the future.
Check Value On Data Receipt:
Whenever you receive data, check its source.
If the data is from a connected component and is undefined or not present, use the stored default value. If the data is another IP, update the stored default value.
Here's a potential code snippet to help visualize this:
The logic above assumes that the component can distinguish the source of its data, and responds accordingly. The "event system" or subscription model will depend on how you've structured data flow in your application, so adjust accordingly.
Human:
Does noflo have API to distinguish an IP is sent by `fbpGraph.addInitial(value, node.id, key);`?
GPT4:
As of my last training data in September 2021, NoFlo uses the concept of "Initial Information Packets" (IIPs) to pass initial data into a flow graph when it starts. When using the
addInitial
method, you're essentially adding an IIP to the graph.However, natively distinguishing between an IIP and a regular Information Packet (IP) sent during the flow's operation isn't explicitly supported out of the box by NoFlo. In the standard flow of NoFlo, IIPs are sent once when the network starts, and they're just like any other IP after that.
That said, if you need to distinguish between them in your application logic, there are workarounds:
Metadata in the IP: As previously suggested, you can modify the structure of the IP itself to include some metadata. This requires that every component understands this structure, which might not be ideal if you're working with existing components or if you're aiming for a clean separation of concerns.
Custom Initialization Process: You could customize the graph initialization process to send IIPs with a recognizable pattern or tag. For instance, you could prefix IIP data with some string or wrap it in a specific way, then remove or unwrap it in the first component that handles it.
Component State: Components could keep an internal state flag indicating if they've received an IIP yet or not. When they first receive data, they'll know it's the IIP. This approach, however, assumes that an IIP will always be sent before any other IPs, which is typically the case but might vary depending on your flow's logic.
Custom Component for IIP Handling: Create a custom component that only deals with handling and tagging IIPs. The graph sends IIPs to this component, which then tags them and forwards them to the rest of the network. This is a more modular approach and keeps the IIP handling logic separate from other components.
If NoFlo has introduced new features after 2021 or if there have been other developments in the community to address this, you might want to check the latest NoFlo documentation or community forums.
Human:
I'm using
fbpGraph.addInitial(value, node.id, key, { initial: true });
where its type is(method) Graph.addInitial(data: any, node: string, port: string, metadata?: PropertyMap | undefined): Graph
, so I added metadata here. How to get this metadata in process method of noflo component?GPT4:
If you're adding metadata to the Initial Information Packet (IIP) using
fbpGraph.addInitial()
, you can access this metadata inside the component. Here's a step-by-step guide to achieve this:You've already done this:
In a NoFlo component's
process
function, you have access to the Information Packet (IP). The metadata you attached to the IP can be accessed from this IP object.Here's a simplified example of how you might do this in a component:
In this example, we get the IP using
input.get('in')
. The metadata that you've attached when setting the IIP can then be accessed withip.metadata
. You can then use this metadata in your logic as required.Remember, always ensure that the IP exists and that the metadata property you're trying to access is present before using it to avoid potential errors.
Human:
导出时间 2023年8月18日 下午3:53.
The text was updated successfully, but these errors were encountered: