Embedding Web Edit in an iframe

Update 2019-12-31: A new and improved method, thanks to Madhav Kannan

While the below post still works, Tableau’s Madhav Kannan has developed a newer technique that doesn’t require any additional files be added or hosted on the Tableau Server. Highly recommended that you look to his solution first, and then if you need the slight additional features of the method below, consider if it is worth the effort and maintenance.

Update 2017-04-17: Big hat-tip to Tableau’s Santiago Sanchez, who gave the heads up that an update was needed for Tableau 10.2. If you’ve upgraded and things aren’t working, please download the new versions available on GitHub. There are other updates as well to handle the full range of behaviors of the web edit screen.

Update 2018-11-02: Second hat-tip to Tableau’s Ron Dugger who found where you can put this file in 2018.2 and later versions of Tableau Server. Things move, we find them!

It’s pretty simple to embed a Tableau viz into a web page. You can do it directly with an iframe, use the simple JavaScript API embed available with the “Share” button, or do the full JavaScript API embed. However, when you embed a viz, when the Web Edit button is enabled, the default behavior is for a second window to open up with the Web Edit view, which looks similar to Tableau Desktop (another post about cleaning up general ‘theme’ will be forthcoming).

The main issues with this popup window are:

  1. The URL bar in a browser will show the Tableau Server location rather than your web application
  2. The separate window can break the intended UX flow
  3. When the user is Done, the window closes, and it returns to the original page with the embedded viz. But nothing in that viz changes — and since most Web Edit will result in a Save As, the user is now looking at an unchanged viz that is no longer even the correct workbook.

Note: Everything that follows is completely unsupported by Tableau Support.

If you’ve read the post about Embedding in a C# Application, you’ll have seen that Web Edit can be triggered off ‘manually’ by going to locations with the following pattern:

http://{TableauServer}/t/{site}/authoring/{wbname}/{dashboard/sheetname}

This means you can open a Web Edit view directly in an iframe. There’s a reason those posts are specifically about a C# application: outside of the embedded web part available in the Microsoft stack, you cannot detect the URL location of separately opened window from the originating window. Due to the restrictions on Cross-Domain Scripting inherent in all browsers’ security models, the parent page of an iframe that comes from anywhere else (and this means anything with as different port number or subdomain) cannot see the url location of that iframe, even if it has changed.

The one exception is if both of the pages come from the exact same domain, subdomain and port. In this one case, you can get to the content in the iframe using the ‘contentDocument’ property of the DOM . Why does this matter? The main challenge with embedding Tableau’s Web Edit is determining which View the user should be returned to when they press Done, while at the same time ignoring any actions that leave the user in Web Edit mode. Because a Save As could happen along the way, the content the user was editing may be located somewhere different than what was originally loaded into the iframe (or your original embedded view).

embed_wrapper.html – on the Tableau Server

How do we get on the exact same location at the Tableau Server so we can do this magic trick? By adding an HTML file (we’ll call this the ’embed wrapper’) to the vizportal content directories on the Tableau Server. Remember: this will get removed on each upgrade, so keep a copy and have a process for reintroducing after upgrades.

Prior to Tableau 2018.2:

The easiest directory to put the file in is C:\Program Files\Tableau\Tableau Server\9.2\vizportalclient\public\en\ (remember to change for your version number).

2018.2 and later versions:

Put the file in C:\ProgramData\Tableau\Tableau Server\data\tabsvc\httpd\htdocs

(If you install to somewhere other than default, this directory will probably exist under that directory structure somewhere)

(If you are using Tableau Server for Linux, let us know if there is anything additional or what the standard directory would be)

I’ve created an example file and named it embed_wrapper.html; it is available on GitHub. The only things you should need to change are the domain variable and possibly the size of the iframe you want to create. Update 2016-03-22: The version on GitHub now handles Sites that are not the default. Please update to the more recent code if you are using the earlier version

You can detect any time an iframe changes location by placing a function on the ‘onload’ event listener of that iframe. This actually fires off on the original ‘onload’ event when you first create the iframe, so to make it useful you need a counter so you can ignore the first one. Since we are on the same domain, at this onload event, we can grab the new location that the Web Edit has moved to.

The final bit of magic is that two pages not on ‘exactly’ the same domain can declare themselves to be on the same domain using the document.domain property, like so:

document.domain = ‘mydomain.com’;

If both pages have declared they are in the same domain this way, then an iframe created by the parent page can access the DOM of the child, and vice-versa. Now, if you declare the embed wrapper page to be on a particular name at the beginning, it won’t be able to talk to the embed Tableau Server web edit viz. Amazingly, you can wait until after you get the URL from the Tableau Server iframe, then declare the document.domain to now match the outermost page (the web application which is embedding the embed framework page).


edit_iframe.onload = function(){

if (edit_iframe_load_count > 0){
// Corrects for even if IE8 isn't in standards mode
var y = (edit_iframe.contentWindow || edit_iframe.contentDocument);
url_on_load = y.location.href;
//alert("new url: " + url_on_load + "\n" + "original url: " + edit_frame_url);
// Capture when try to load view based on the fragment
if ( url_on_load.search('/#/views') !== -1 || url_on_load.search('/#/site/') !== -1 || url_on_load.search('/t/') !== -1  ){
/*
Most essential part. This switches the declared domain so that the iframe
is now in the same "declared domain" as the originating page. This allows the
next call to the function in the parent page

Both parent page and this page must declare the same domain listed here.

*/
document.domain = declared_domain;

// This function is declared in the application page
parent.iframe_change(url_on_load);
}
}
edit_iframe_load_count++;
}

full_embed_including_web_edit – Your Application Page

You’ll notice at the end of that last bit of code, that there is a ‘parent.iframe_change()’ function call. That function actually lives in your application page that embeds the (non-web edit) viz. I’ve included an example called full_embed_including_web_edit.php, available on GitHub . Don’t worry, it’s only PHP to do some trusted tickets action, which is all turned off in the code. You can easily strip it down just to the HTML and JavaScript you need.

There are only two important functions here. The first is ‘launch_edit()’, which is what you hook up to your own Web Edit button (I will cover this more in another post, but part of this level of embedding assumes you will be hiding either the whole toolbar on the Tableau viz or at least the web edit button in the toolbar).

The main thing is just to point to the embed_wrapper page and pass the URL of the viz to the page using the ‘src=’ parameter (you are free to rename this to anything, just make sure to change both files).


function launch_edit(){
$("#tableauViz").hide();
var edit_location = 'http://{yourdomain.com}/en/embed_wrapper.html?src=' + vizUrl; // Change for yours
edit_iframe = document.createElement('iframe');
edit_iframe.src = edit_location;

// This makes it not look like an iframe
edit_iframe.style.padding = '0px';
edit_iframe.style.border = 'none';
edit_iframe.style.margin = '0px';

// Also set these with the same values in the embed_wrapper.html page
edit_iframe.style.height = '800px';
edit_iframe.style.width = '1200px';

document.body.appendChild(edit_iframe);

}

Lastly, when the embed_wrapper page calls, we want to close out the web edit iframe, destroy the original viz (which we just hide originally), and create a new viz object using the URL passed from the embed_wrapper.


function iframe_change(new_url){
   // Destroy the original edit_iframe so you can build another one later if necessary
            $(edit_iframe).remove();
            // Destroy the original Tableau Viz object so you can create new one with URL of the Save(d) As version
            viz.dispose();
            
            // Reset the global vizURL at this point so that it all works circularly
            //viz_url = new_url;
            console.log("New URL should be: " + new_url);
            
            var url_parts = new_url.split('?');
            viz_url = url_parts[0];
            if (viz_url.search('authoring') !== -1){
                console.log('Found an authoring url');
                vizUrlForWebEdit = viz_url; 
                launch_edit();
                return;
           }
            // Handle site
            if (viz_url.search('/site/') !== -1){
                var url_parts = viz_url.split('#/site/');
                viz_url = url_parts[0] + "t/" + url_parts[1];
                vizUrlForWebEdit = viz_url;
            }
            console.log("New URL should be: " + viz_url);
            
            // Create a new Viz object with the new URL
            $("#tableauViz").show();
            console.log("tableauViz div now is shown");
            viz = new tableau.Viz( document.getElementById("tableauViz"), viz_url, options);
            console.log("Viz should be created");
     
}

Caveats – Updated 2016-04-28

I’ve tested this on Chrome, IE and Firefox and it works with all of them. For this to work, your domain has to be explicitly declared everywhere on every server, even if you can access the server without the domain internally. As long as the Tableau Server and the web application server are both on the same domain, and you declare the part that they both share (i.e. domain = ‘yoursite.com’), the sessions will all match up and it works.

You also need to turn off click-jacking protection, which is on by default starting with 9.2. And of course, to see web edit at all (and the subscription options), you need to enable Unrestricted Trusted Tickets.

You don’t have to worry about Trusted Tickets with Web Edit so there is nothing else to add there, the session that is created will be assumed. If the regular viz embed works with trusted auth, the web edit page will just use the session cookie that is already created.

There also seems to be one other edge case where the Web Edit screen does not go back to the newly created viz but instead goes to the Projects page in Tableau Server. Once I track it down, I will add logic to redirect correctly.

10 comments

  1. Have you found a way to get this to work with the web site and tableau on two different servers? My environment is set up this way and y.location.href in the embed_wrapper page throws an ‘Access Denied’ error. Somehow it doesn’t look like it’s using the parent page document domain.

    Liked by 1 person

    1. Hi Kevin,
      I just got through testing it on our internal network, where the web server is separate from the Tableau Server. I updated the “Caveats” section with a few pieces that were required, but in the main, it was making sure that the shared domain was explicit in every place. So if you internal domain is ‘mydomain.lan’ then you need to access the web server like “webserver.mydomain.lan” and set any reference to the Tableau Server like “tableauserver.mydomain.lan”. Once that is all in place, the domain declaration works and it all ties together.

      Like

      1. Hi Bryant,

        Thanks for responding to my comment. Since I posted this back on 3/23, I have been able to get this working in my environment without the CORS issue. After reading another blog, I decided to add embed_wrapper.html as a web data connector in Tableau server. This allowed me to execute the following code within the wrapper page and pull back the underlying location of the iframe, without that pesky ‘Permission/Access Denied’ error.

        var y = iframe.contentWindow || edit_iframe.contentDocument);
        url_on_load = y.location.href;

        Now, in the container page on my website, the callback function is executed, and I can determine what the new viz URL is.

        Like

  2. Hi Bryant

    I am facing similar kind of issue. Can we do this trick without using the iframe for embedding views into the tableau server. Here in our case we are using javascript embed code and putting that code in our website to embed. The same issue when we click on edit button , it opens up in a new window.Is there any possible solution that we can do this with javascript embed code or we have to use iframe way.

    Like

      1. Hi Bryant,

        Have you had a chance to test embedded web edit with Tableau Server 10.5? This approach has worked well from 9.2 through 10.2. We just upgrade our QA environment to 10.5, and now we get an “Unable to connect to the data source; No Tableau User found” error when the iframe_change(new_url) function is executed. We’ve never had to modify the new_url value that’s returned. So, viz_url has always been set to the value of new_url.

        The strange thing is if we actually edit a viz in the embedded web edit iframe, then returned the new_url is rendered without error when iframe_change is called.

        Like

Leave a comment