Calling a connector function from within a custom widget?

Hi, I was just wondering if this is possible?

The connector function is configured to retrieve a fairly complex json via a 3rd party api securely.

As I understand it you would normally call the connector function via a trigger in a app, and then try save that data into a variable and then map the variable properties into properties on the widget.

But what if you have lots of similar widgets on the application? It feels like I would need to create lots of duplicate variables with the same mapping which feels very clunky.

Appreciate any ideas to make things simpler.
Thank you in advance.

2 Likes

Hey @John_L this is a great question. Could you tell me more about your use case here?

I would like to present additional information that comes from a 3rd party api. There is data that is specific to each machine, and the idea is to have an application that can have multiple custom widgets (per machine) on the dashboard. So to avoid lots of configuring for each widget, I would like to call the connector function from within the custom widget and update the widget content using javascript. thank you.

Hey @John_L

This is an interesting application. I could think of a few ways to solve this:

  1. Definitely, you could just call the API within the widget itself. This might give you the most flexibility in that you donā€™t need to make new Tulip variables.

  2. If you need to use a Connector, you could have an ā€˜eventā€™ in your widget that, at some point, fires. When that Event fires, a Trigger on the widget could call the Connector and do something.

  3. Depending on how complex the output is, if you set the Connector to run globally, sometime like ā€œon app openā€, you could forego the complex output parsing and instead do this in the Widget (in other words, make the Connector just output a large config JSON file and pass this single object into all your Widgets, parsing via Javascript)

Are any of these methods useful?

  1. Definitely, you could just call the API [within the widget itself]

a/). This might give you the most flexibility in that you donā€™t need to make new Tulip variables.

These custom widgets run within an iframe, and there are security boundaries that prevent the widget from accessing the parentā€™s ecmascript context. Iā€™m still thinking that I want more than what you are describing here. For one thing, if you do as you describe you need to now deal with permissions. It would be better if you could just have the widget have the same permissions the logged in user has. Itā€™s also more code and complexity to do this reacharound.

I canā€™t remember if Iā€™ve already described this, but long term what I really would like is this: create some kind of internal API that lets you do basic things like read/write tables, machine status, etc. then you put this API into an object and inject it into the iframe. This way the widget can just use it. So for example you create an object

class WidgetApi {
    write_table( ... )
    read_table( ... )
    get_machine_state( ... )
}

I donā€™t know what that API is (this is just notional) but what you then do is inject an instance of that into the widgetā€™s iframe. Then the widget can just call that api.

I really didnā€™t like the idea of having to deal with bot logins, permissions, API keys, etc. for this. I know itā€™s an artifact of the fact that the widget is strongly sandboxed, which is good. Iā€™m just suggest thereā€™s way to poke a very controlled hole through that sandbox by just passing an API wrapper object into it.

2 Likes

@wz2b super valid, and tbh something Iā€™ve run into myself (for ex, when certain API just really dislike the null origin headers given by widgets). Passing this along to the Connectivity team, as this is a similar fix to the way we improved Tulip Tag Nodes in Node-RED by using the onboard device certs, rather than needing bot keys. Thanks for the notes!

thank you for the feedback, I have multiple widgets on the same application and each is displaying additional data for each machine (data that needs to be fetched frequently)ā€¦ I understand I could call the api directly within the custom widget, but then I would need to store the auth header somewhere so that the widget can use it. The connector functions purpose is to configure everything required to call the external api (including auth). Just thought it would be cleaner if the widget could just execute the connector function internally from the javsacript and handle the parsing/display of the json data internally. thanks.

@k.ober I get the following CORS error when I call the api directly from inside the custom widget. The 3rd party api is only accepting requests from a certain origin and the origin does not seem to be included in the request. Any ideas? thank you.

@John_L ah, yep, Iā€™ve encountered this exact issue before. The null origin occurs due to the sandboxing around Custom Widgets inside Apps - this can sometimes cause CORS errors on the receiving end.
Unfortunately, this is a known limitation of Custom Widgets - this origin cannot be changed. If itā€™s an internal API, then we have found solutions before with allowing null origins - otherwise, this is a known security-based limitation of the system.

Iā€™ll pass this on to the engineering team as product feedback, for sure.

thank you for the feedback, it means the 3rd party api will unfortualtey have to allow null origin which is a security risk. This is another reason I think it would be great if we could invoke the connector function from inside the custom widget.

I am appreciating the conversation around this thread.

As we talk about an internal API that custom widgets can access, I agree especially with the concern about authentication. I donā€™t see any problem if a widget wants to make a call back to the API independently, but dealing with all the issues we discussed already - authentication and CORS being two - I think having an API object injected that uses the hosting web pageā€™s credentials is still the right thing to do. That gets the widget out of having to store or even understand anything about authentication.

I have another suggestion as well. If weā€™re going to do this, the most important things a widget needs to be able to access are

  • Any record placeholder belonging to the host step
  • Tables

For table access, I had another thought. It would sure be nice to have some table related helper methods to be able to do things like refer to tables and columns using the column name in addition to by object ID. Maybe for columns itā€™s less important because you can return that with the table query response metadata, but finding the right table by its name requires a piece of code I find myself replicating over and over.

I will say, though, the place to start isnā€™t tables, itā€™s placeholders. An internal API might look like:

interface WidgetApi {
   IQuery getQueryByName (name: str)
   ITable getTableByName(name: str)
   ITable getTableById(id: str)
   /* ... more ... */
}   

interface IQuery {
   Field getField(name: str)  /* get value of one field */
   QueryRow[] getPlaceholder()   /* get values of all fields */

   bool setField(name: str, value: any)  /* set one field's value */
   bool setFields(newValue: QueryRow[])  /* set all fields in QueryRow */
}

interface QueryRow {
   string getName()
   string getId()
   any getValue()
}

Excuse my typescript please :slight_smile:

So the thought here is, when you create an IFrame, inject a reference to a WidgetApi into that iframe that the widget can just useā€¦ for starters, to manipulate record placeholders, since I think that would be the most widely used, then from there, direct access to tables, then eventually machines and everything else.

In a way, I know that some of these APIs already exist. You can start a browser console and watch the ā€˜Networkā€™ tab and see how the step gets all its data. From what I have observed, though, I think containing that with a simpler facade would be wise.

Is this thread of conversation at all helpful?

ā€“Chris

2 Likes

Add async executeConnectorFunction(ā€œByNameā€,{}): any to the api list too :slight_smile:

1 Like

Thatā€™s a good idea.

The entire API I outlined probably should be async, now that you mention it. I was just thinking functionally to start. Everything I outlined above really should return a promise.

1 Like

Hey, Iā€™m currently working on a task which uses same method to store the connector output in a variable in Tulip App & then pass the data in Custom widget.

Issue: - Iā€™m able to store connector output (JSON) in a variable but when Iā€™m passing the variable in custom widget using Props, Data props is receiving is NULL. I even created Event & try to pass data, but the custom Widget is Stil showing NULL.
Just to test, I created a text variable with default value, but Props are unable to read data.

Can you tell me where Iā€™m making any mistake in steps?

What type are using for your connector function output? Can you confirm that the application is receiving the value from the connector function (before sending it to the custom widget) by message displaying the variable you assigned the output of the connector function to in the app?

Iā€™m using a HTTP connector to receive data from API & generate JSON as an output. After storing data in a variable, i tested whether data is getting stored in variable or not but the data is getting stored in a variable.
Iā€™m attaching the screenshots. tell me where Iā€™m making any mistake.
Variable in Application:


Output:

Widget call in App:

Custom Widget Props:

Custom Widget Output:

Custom Widgets Overview (tulip.co)

try using

getValue(ā€˜HQā€™, (value) => {
console.log(value)
});


getting error

Those donā€™t look like proper single quotes around HQ :slight_smile:

getValue(ā€˜myPropā€™, function(value){
console.log(JSON.stringify(value));
});

getValue(ā€˜myPropā€™, (value) => {
console.log(JSON.stringify(value));
});

Hi @John_L,

@rahul.singh just copied your code. Unfortunately this page converts single quotes like 'these' into ā€˜thisā€™.

Try to use the ā€œCodeā€ option for those applications

image

This would be the result:

getValue('HQ', (value) => {
console.log(value)
});

getValue('myProp', function(value){
console.log(JSON.stringify(value));
});