Community Spring Cleaning week is here! Join your fellow Maveryx in digging through your old posts and marking comments on them as solved. Learn more here!

Dev Space

Customize and extend the power of Alteryx with SDKs, APIs, custom tools, and more.
SOLVED

HTML SDK - Password persistency w/out Widgets

Coxta45
11 - Bolide

I'm working on building a new version of the JIRA connector using the HTML/JavaScript SDK.  I've run into an issue with persistency on a SimpleString password data-item.  While the widgets are fine, I'm a web developer at heart and am enjoying being able to use a light build of my favorite front-end framework to design the UI for the connector (having this capability is by far my favorite part of the new SDK at this point).

 

Ok, so here is the problem.  Without using the widgets, you can't make use of the nifty bindDataItemToWidget function within the manager class...which means you're responsible for handling the data persistency between the XML config and the UI on your own.  This only causing me issues when I'm handling a SimpleString of the password type.  Without specifying the data type as password, the persistency is a piece of cake.  However, the password is stored in plain sight, unencrypted, right there in the XML.  If I do specify the SimpleString as a password, then I lose the password on it's way back to the UI (Alteryx.Gui.AfterLoad) from it's encrypted format in the XML.  I feel like I am missing something...Is the password encryption one way?  Is there a decryption helper function that I'm missing?  Here's what I've got so far...

 

HTML Markup (input fields)

<!-- Site URL -->
<div class="field">
  <label for="siteUrl">Site URL</label>
  <input placeholder="https://example.atlassian.net" id="siteUrl" name="siteUrl" type="text">
</div>

<!-- Username or Email -->
<div class="field">
  <label for="userName">Username or Email</label>
  <input placeholder="your.username" id="userName" name="userName" type="text">
</div>

<!-- Password -->
<div class="field">
  <label for="userPass">Password</label>
  <input placeholder="your password" id="userPass" name="userPass" type="password">
</div>

JavaScript (BeforeLoad, AfterLoad functions)

Alteryx.Gui.BeforeLoad = function (manager, AlteryxDataItems, json) {

	// JIRA Site URL
	var siteUrl = new AlteryxDataItems.SimpleString('siteUrl')
 	manager.addDataItem(siteUrl)
	document.getElementById('siteUrl').onkeyup = function (siteurl) {
		siteUrl.setValue(siteurl.target.value)
	}

	// Username
	var userName = new AlteryxDataItems.SimpleString('userName')
 	manager.addDataItem(userName)
	document.getElementById('userName').onkeyup = function (username) {
		userName.setValue(username.target.value)
	}

	// Password ... Is this one way encryption???
	var userPass = new AlteryxDataItems.SimpleString('userPass', {password: true})
 	manager.addDataItem(userPass)
	document.getElementById('userPass').onkeyup = function (userpass) {
		userPass.setValue(userpass.target.value)
	}

}

Alteryx.Gui.AfterLoad = function (manager, AlteryxDataItems) {

	var siteUrl = manager.getDataItem('siteUrl')
	var userName = manager.getDataItem('userName')
	var userPass = manager.getDataItem('userPass')

	document.getElementById('siteUrl').value = siteUrl.getValue()
	document.getElementById('userName').value = userName.getValue()
	document.getElementById('userPass').value = userPass.getValue()
        // userPass.getValue() returns empty string, others return their respective values
}

Anyone have any ideas? @jdunkerley79, I've been following your blog and you seem to know your way around the SDK as well as anyone at this point...any chance you have any advice here?

 

Regards,

 

Taylor Cox

 

15 REPLIES 15
jdunkerley79
ACE Emeritus
ACE Emeritus

Hey Taylor:

 

See below for example setting it all up. Stuck in one file but I'd have as two by default:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello From JavaScript</title>
    <script>
        document.write(`<link rel="stylesheet" type="text/css" href="${window.Alteryx.LibDir}1/css/alteryx-config.css">`)
        document.write(`<link rel="stylesheet" type="text/css" href="${window.Alteryx.LibDir}1/lib/alteryx/gui/plugin-widgets/alteryx-desktop-widgets.css">`)
        document.write(`<link rel="stylesheet" type="text/css" href="${window.Alteryx.LibDir}1/lib/build/designerDesktop.css">`)
        document.write(`<script src="${window.Alteryx.LibDir}1/lib/build/designerDesktop.bundle.js">\x3c/script>`)
    </script>
    <script>
        function setupItem(manager, AlteryxDataItems, name, password) {
            if (!manager.GetDataItemByDataName(name)) {
                manager.AddDataItem(new AlteryxDataItems.SimpleString({ dataname: name, id: name, initialValue: '', password: (password ? 'true': '') }));
            }
        }

        function wireUpTextbox(manager, name) {
            const item = manager.GetDataItemByDataName(name)
            const element = document.getElementById(name)
            element.value = item.getValue()
            element.onkeyup = (event) => item.setValue(event.target.value)
        }

        Alteryx.Gui.BeforeLoad = function (manager, AlteryxDataItems, json) {
            setupItem(manager, AlteryxDataItems, 'siteUrl');
            setupItem(manager, AlteryxDataItems, 'userName');
            setupItem(manager, AlteryxDataItems, 'userPass', true);
        }
        
        Alteryx.Gui.AfterLoad = function (manager, AlteryxDataItems) {
            wireUpTextbox(manager, 'siteUrl')
            wireUpTextbox(manager, 'userName')
            wireUpTextbox(manager, 'userPass')
        }
    </script>
</head>
<body>
    <form>
<!-- Site URL -->
  <div class="field">
    <label for="siteUrl">Site URL</label>
    <input placeholder="https://example.atlassian.net" id="siteUrl" name="siteUrl" type="text">
  </div>
  
  <!-- Username or Email -->
  <div class="field">
    <label for="userName">Username or Email</label>
    <input placeholder="your.username" id="userName" name="userName" type="text">
  </div>
  
  <!-- Password -->
  <div class="field">
    <label for="userPass">Password</label>
    <input placeholder="your password" id="userPass" name="userPass" type="password">
  </div>
    <form>
</body>
</html>

Doesn't use the plugin widgets but uses the manager. Password will be stored encrypted (there are a few quirks to the data items!). 

 

UI looks like:

2017-10-27_18-28-21.jpg

 

Resulting in a configuration of:

<Configuration>
  <Value name="siteUrl">http://www.google.co</Value>
  <Value name="userName">jdunkerley</Value>
  <Value name="userPass">40CF28641A05AAA25911179DFD26A4BB78D82002D755C4FED37D7502FC3B1190540F9170A878D715AAB1911A90F89CF0F26A8B9207517CB7B6591B34E6AC89192C39C7AE9C2B79A11D78C5BD3106D45BBA6A5068C4EE474D7616E541603C992FB3D3788C985095284854EE4E639CA</Value>
</Configuration>

Hopefully enough for you to pull apart but happy to help as you need

 

Best

James

Coxta45
11 - Bolide

Thanks, @jdunkerley79 - this works like a charm.

 

I'm still a little perplexed in trying to understand why my original attempt won't work.  To be more clear, I was using 2/lib/build/designerDesktop.bundle.js

document.write(`<script src="${window.Alteryx.LibDir}2/lib/build/designerDesktop.bundle.js">\x3c/script>`)

(v2 of SDK?) - Not sure what the difference is at this point.  I noticed you were using 1/lib/build/designerDesktop.bundle.js

 

Anyway, using 2/lib/build/designerDesktop.bundle.js with the original JavaScript above, here is the behaviour.

 

New Tool instance saves DataItems to Manager and writes to config with no issues:

BeforeBeforeConfigConfig

But loses the Password in AfterLoad...(encrypted pass is still visible in the config)

After LoadAfter Load

Again, a bit perplexed about why that is happening and I'm sure I've missed something.  For now, however, I'll switch over to the 1/lib/build/designerDesktop.bundle.js and begin using your refactored approach (TypeScript, I assume?)

 

Thanks a bunch!

jdunkerley79
ACE Emeritus
ACE Emeritus

v1 of SDK is the version used by the formula tool (and the one I which v11.0 gave away all the details of). 

v2 is the official Beta

 

I did a quick adjustment to my code to work with v2:

        function setupItem(manager, AlteryxDataItems, name, password) {
            if (!manager.getDataItem(name)) {
                manager.addDataItem(new AlteryxDataItems.SimpleString(name, { password: !!password }) );
            }
        }

        function wireUpTextbox(manager, name) {
            const item = manager.getDataItem(name)
            const element = document.getElementById(name)
            element.value = item.getValue()
            element.onkeyup = (event) => item.setValue(event.target.value)
        }

        Alteryx.Gui.BeforeLoad = function (manager, AlteryxDataItems, json) {
            setupItem(manager, AlteryxDataItems, 'siteUrl');
            setupItem(manager, AlteryxDataItems, 'userName');
            setupItem(manager, AlteryxDataItems, 'userPass', true);
        }
        
        Alteryx.Gui.AfterLoad = function (manager, AlteryxDataItems) {
            wireUpTextbox(manager, 'siteUrl')
            wireUpTextbox(manager, 'userName')
            wireUpTextbox(manager, 'userPass')
        }

Only substantial difference I can see is that I don't read the values from the manager until AfterLoad. I also wire up the event handlers there rather than in BeginLoad. Think this means it has been told to read the config in password mode.

 

Nope didn't use TypeScript for this just ECMAScript 6. As they use CEF inside the GUI can use all the ES6 features you like.

 

James

Coxta45
11 - Bolide

Any idea how to persist an array of data (such as an API response)?  I've looked in to DataItemContainers, but haven't had any luck constructing one.

 

Example array to persist: projectsArr

[
	{ key: 'DEV', name: 'Development', avatar: 'https://any.thing?id=123&size=xsmall' },
	{ key: 'UAT', name: 'Testing', avatar: 'https://any.thing?id=456&size=xsmall' },
	{ key: 'PROD', name: 'Production', avatar: 'https://any.thing?id=789&size=xsmall' }
]

Essentially, I'd like to store/persist some metedata and only make the API call if and when it needs refreshing - as opposed to calling the endpoint to refresh this list every single time Alteryx.Gui.BeforeLoad instantiates.  I also tried storing the array in the optionList of a StringSelector but the options didn't persist and it limits to only a key-value pair (well, label-value actually).

 

Don't care for this and the optionList didn't persist...

var configArray = function configArray(manager, AlteryxDataItems, name, array) {
	if (!manager.getDataItem(name)) {
        manager.addDataItem(new AlteryxDataItems.StringSelector(name, { optionList: array }) )
    }
}
RyanSw
Alteryx Alumni (Retired)

With the SimpleString as a password data item, there's something a little bit transparent going on. Everything you have here actually looks really good for non-widget usage!

 

The issue you're running into though, is that the password get's decrypted by the Alteryx Designer code outside of the JavaScript environment, which will then send the JS back the appropriately decrypted password once it's done. This process is however asynchronous, so with SimpleString as a password, we have the single use case where you won't always have the value of the data item by the time AfterLoad executes.

 

Not to worry though! The eventful nature of this is exactly why we have property registration on the data items in the v2 SDK!

 

In BeforeLoad you want to set a listener to update your input element when the value changes, since it will change at a non-deterministic time (when Alteryx has finished decryption).

 

Tossing this into your BeforeLoad should work:

 

 

userPass.registerPropertyListener('value', function(propertyChangeEvent) {
  document.getElementById('userPass').value = propertyChangeEvent.value
})

 

NOTE:

The differences between v1 and v2 are significant! We strongly encourage people to use v2 over v1, and v2 is well documented here:

 

https://help.alteryx.com/developer/current/index.htm#HTML/UseSDK.htm%3FTocPath%3DCustom%2520Alteryx%...

 

The contents expand from the sandwich button on the top left side, when you expand "contents" you will see the rest of the documentation pages!

 

Hope this helps, and this little detail about the password portion of data items will be added to the documentation!

RyanSw
Alteryx Alumni (Retired)

If you want to store arbitrary data that isn't structured by data items available, we give you a hook right before the Designer requests the tools configuration to write to the XML where you can change the configuration object structure and data called `BeforeGetConfiguration` - this is called right as you're clicking off the config pane when it's going to get the config to store in the workflow.

 

Here's a quick example:

 

window.Alteryx.Gui.BeforeGetConfiguration = function(configObj) {
  configObj.Configuration.SomeData = [{foo: { bar: "arr", baaz: 3 }}, {foo: { bar: "arasdfr", baaz: 2 }}]
  return configObj;
}

To then get your object back, given data items can't load it, just pull it off the json in BeforeLoad:

 

Alteryx.Gui.BeforeLoad = function (manager, AlteryxDataItems, json) {
  console.dir(json.SomeData);

 

NOTE:

Remember to return the config object from BeforeGetConfiguration! It's a common easy step to overlook, if you don't return the object from BeforeGetConfiguration your configuration will not be stored in the XML at all, and you may see some timeouts where the Designer is waiting for the config object to be given to it.

jdunkerley79
ACE Emeritus
ACE Emeritus

I still prefer some aspects of the v1 API over v2 but in general, can do same things in both. I do need to update my typings file for v2.

 

As a quick example I added a list of items to the UI:

2017-11-01_08-05-08.jpg

 

Basically, I chose to keep a data item inline with the UL via JSON serialization on each change. Seemed to work pretty well

 

Script looks like:

        function setupItem(manager, AlteryxDataItems, name, password) {
            if (!manager.getDataItem(name)) {
                manager.addDataItem(new AlteryxDataItems.SimpleString(name, { password: !!password }) );
            }
        }

        function wireUpTextbox(manager, name) {
            const item = manager.getDataItem(name)
            const element = document.getElementById(name)

            item.registerPropertyListener('value', (propertyChangeEvent) => element.value = propertyChangeEvent.value)

            element.value = item.getValue()
            element.onkeyup = (event) => item.setValue(event.target.value)
        }

        Alteryx.Gui.BeforeLoad = (manager, AlteryxDataItems, json) => {
            setupItem(manager, AlteryxDataItems, 'siteUrl')
            setupItem(manager, AlteryxDataItems, 'userName')
            setupItem(manager, AlteryxDataItems, 'userPass', true)
            setupItem(manager, AlteryxDataItems, 'dataitems')
        }
        
        Alteryx.Gui.AfterLoad = (manager, AlteryxDataItems) => {
            wireUpTextbox(manager, 'siteUrl')
            wireUpTextbox(manager, 'userName')
            wireUpTextbox(manager, 'userPass')

            const itemsElement = document.getElementById('items')
            const dataitems = manager.getDataItem('dataitems')
            const createItem = (text) => `<li onclick="removeItem(this)">${v}</li>`
            window.removeItem = (item) => { 
                itemsElement.removeChild(item)
                dataitems.setValue(JSON.stringify([...itemsElement.children].map(e => e.innerHTML)))
            }
            if (dataitems.getValue()) {
                JSON.parse(dataitems.getValue()).forEach(v => itemsElement.innerHTML += createItem(v))
            }
            const itemElement = document.getElementById('item')
            itemElement.onkeyup = (event) => {
                if (event.keyCode === 13) {
                    itemsElement.innerHTML += createItem(v)
                    itemElement.value = ""
                    dataitems.setValue(JSON.stringify([...itemsElement.children].map(e => e.innerHTML)))
                }
            }
        }

and new UL in body:

        <!-- Demo List -->
        <div class="field">
            <ul id="items"></ul>
            <input placeholder="Item to add" id="item" name="item" type="text">
        </div>

 

As a side note: @TashaA / @LeahK - we badly need to be able to attach html/js/zip and pretty much whatever devs need to these threads

Coxta45
11 - Bolide

@RyanSw Thanks for the tip regarding the asynchronicity!  Probably a good idea to get that documented sooner rather than later :-) Nonetheless, it was an easy fix to the problem at hand and I've gone back to v2 of the SDK.

 

Per using the BeforeGetConfiguration and AfterGetConfiguration methods...

 

How would one go about updating this "later on".  For example, given this form, a user logs in (connects to JIRA) and I immediately call a project API endpoint that returns a list of available projects for the user.  When I get the json back (asynchronously) from the call, I'd like to SetConfiguration and update the list of available projects (config shown below).

 

Is this where window.Alteryx.JsEvent methods come in handy? Obviously, window.Alteryx.Gui.BeforeGetConfiguration has long been called at this point.  Again, the goal is to not have to call the API to get a list of projects every single time the tool is instantiated, but only when the user either needs to update their login credentials or manually asks for the project list to be refreshed.  Just trying to be agile and lean at the same time..

 

UI

UIUI

 

// User Login
$('#loginForm').submit(function(event){
	event.preventDefault();
	userLogin();
});

 See line # ~27 --> if (loginResponse == 'OK')

var userLogin = function userLogin() {

  message(false, null, null, null)

  $('#loginForm').form('validate form')

  if ($('#loginForm').form('is valid')) {

    loader(true, 'Logging in to JIRA...')

    // Get user input
    var baseUrl = $('#siteUrl').val()
    var uri = baseUrl + '/rest/api/latest/project'
    var auth = 'Basic ' + btoa($('#userName').val() + ':' + $('#userPass').val())

    $.ajax({
        url: uri,
        type: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': auth
        },
        success: function (data, status, response) {

          var loginResponse = response.getResponseHeader('x-seraph-loginreason')

            if (loginResponse == 'OK') {
                updateProjects(data) // I WANT THIS METHOD TO UPDATE THE CONFIG WITH THE PROJECT LIST
                loadPage('projects')
            } else if (loginResponse == 'AUTHENTICATED_FAILED') {
                loader(false,null)
                message(true, 'negative', 'Whoops!', 'Authentication failed.  Double check your username and password.') 
            } else if (loginResponse == 'AUTHENTICATION_DENIED') {
                loader(false,null)
                message(true, 'negative', 'Whoops!', 'Authentication was denied.  This is often due to CAPTCHA triggered from failed login attempts. This can usually be fixed by signing in to JIRA in your browser.  (sign out, first)')
            } else if (!loginResponse) {
                loader(false,null)
                message(true, 'negative', 'Whoops!', 'Authentication failed because the response from <strong>' + baseUrl + '</strong> didn\'t includes the correct headers.  Perhaps the URL was mistyped?')            
            } else {
                message(true, 'negative', 'Whoops!', 'Authentication failed for an unknown reason.') 
                loader(false,null)  
            }

        },
        error: function (data, status, response) {

          loader(false,null)
          message(true, 'negative', 'Whoops!', 'Authentication failed...<br><br>Status: ' + JSON.stringify(status) + '<br>Response: ' + JSON.stringify(response))

        }
    });

 

Ideally this would update the config to look like this:

 Config XMLConfig XML 

Furthermore, to access the XML config object called jiraprojects at any time, is this correct syntax?

window.Alteryx.JsEvent(JSON.stringify({ Event: 'GetConfiguration', Configuration: { Configuration: { jiraprojects } } }))

 

 Lastly... @TashaA / @LeahK ... +1 on @jdunkerley79's suggestion

 

we badly need to be able to attach html/js/zip and pretty much whatever devs need to these threads 
RyanSw
Alteryx Alumni (Retired)

Avoid using JsEvent for now; details at the bottom.

-------------

 

What you describe of attempting to grab data, and persisting it while the config pane is in focus, is something we don't interest in owning within the SDK. Reason being, you can simply put this on the window, or in your own other data store library.

 

I think what you're looking to do is simply pull the data off the config XML in BeforeLoad, and place it somewhere on the window, where you can use it for your own purposes. When you call updateProjects(data), all you should need to do here is update the data you placed on the window at BeforeLoad.

 

Then, to make sure next time they load the config pane, it doesn't need to load all this through ajax again, you can just store it in the XML during BeforeGetConfiguration, by pulling the data from your window variables or other JS data store. You don't need to update the XML at arbitrary times, you can update your window or other live JS data stores at arbitrary times that you use to drive your UI.

 

The XML only needs updating before you leave the configuration pane if the data is for UI caching - so that it is available at next configuration pane load, and before the workflow runs if the data is for the tools back end.

 

BeforeGetConfiguration gives you a place to update your XML that will provide for both the described persistent scenarios. While the configuration pane itself is up and live, you can just use standard means of storing live data.

 

I think something of this nature would deal with what you're trying to do:

 

Alteryx.Gui.BeforeLoad = function(manager. dataItems, json) {
// just as when Ajax came back, update projects from persisted JiraData updateProjects(json.Configuration.JiraData) } updateProjects = function(data) { window.JiraData = data || window.JiraData /* update UI from window.JiraData maybe? */ } Alteryx.Gui.BeforeGetConfiguration = function(configObj) {
// Persist the JiraData for next load so they won't have to run the ajax call on load configObj.JiraData = window.JiraData return configObj }

 

Does this answer your question, and appropriately describe your problem space? Hope it helps; let me know if I'm misunderstanding the problem you're attempting to solve!


-------------


The JsEvent stuff is for calling very specific code inside the designer, the GetConfiguration and SetConfiguration calls on JsEvent are actually lifecycle pieces that act as acknowledgements of lifecycle phase. These are not meant to be called more than once, and if you're using the SDK with the include - it's calling these for you.

 

The JsEvent code could be used for a very small number of specific behaviours by consumers such as encrypt/decrypt or launching a file open / browse dialog, but as it stands the particulars of how these will be consumed by external users is yet to be defined. Some of the thought is that we will publish wrappers (like the simplestring with password flag) for them, but that's yet to be determined.

 

Also, as a general rule, you should unlikely be running Alteryx.Gui.SetConfiguration or Alteryx.Gui.GetConfiguration, as the SDK should manage this for you - and additional calls re-executing lifecycle behaviours may have unintended consequences.

 

If you want to reload your entire configuration through SetConfiguration, I would suggest instead you use the Alteryx.Gui.Manager.fromJson(resolve, reject, configuration) call which will handle deserializing all the JSON into the tree of data items it owns (this is what happens between BeforeLoad and AfterLoad)

 

-------------