Using PPM Timesheets from CA Agile Central when MS Azure SSO used for PPM SaaS

Document ID : KB000116577
Last Modified Date : 28/09/2018
Show Technical Document Details
Introduction:
When using MS Azure for SSO with PPM SaaS, the timesheet integration between CA Agile Central and CA PPM will not work due to limitations and constraints within the MS Azure SSO IDP, in that, it will not allow the PPM timesheet page to open within a frame inside the CA Agile Central browser window.  It will only allow it to be opened in a separate window, and without the CA Agile Central page frame around it.  In order to be able to access PPM timesheets directly from Agile Central in this type of environment, you must build a custom app within Agile Central which provides a link to launch the CA PPM timesheet page in a separate browser tab or window.  This document provides the steps to create the custom application and configure it.
Environment:
CA PPM SaaS (only) / CA Agile Central - Integrated
MS Azure SSO (IDP) for PPM
Instructions:
First, you will need to gather your IDP url based on how you log into CA PPM via the MS Azure Applications Portal page.  For most it will look something like this:

https://myapps.microsoft.com/signin/CA PPM Prod/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?RelayState=https%3A%2F%2Fondemand.ca.com%2Ffedsso%3FtargetUrl%3Dhttps%253A%252F%252FPPMSERVER.ondemand.ca.com

Second you will need the path to the timesheet functionality in the new UI which is as follows:

/pm/#/timesheets

Once you gather that information as noted above, you can then follow these steps:

1. Log into CA Agile Central
2. Click on the "Home" icon, and click the "+" (plus) sign
3. Type in a name for your custom page - such as "CA PPM Timesheets App" as shown here:
User-added image
4. Click Save and Close
5. Click the Home icon again, and then click the newly created page (called CA PPM Timesheets App in our example)
6. On the top right hand side of the page, click on the gear icon, and select "Add App":
User-added image
7. Select the "Custom HTML" type  - click the Add button
8. In the App settings for the new custom html app, fill in the title - for example "CA PPM Timesheets"'
9. In the project section, select the radio dial for "Choose Specific Project" - and in the drop down, choose the appropriate Agile Central project
10. In the HTML section paste the following code:
 
<!DOCTYPE html>
<html>
<head>
    <title>CA PPM Timesheet</title>
    <!--  (c) 2016 CA Technologies.  All Rights Reserved. -->
    <!--  Build Date: Wed Jan 25 2017 15:11:40 GMT-0700 (MST) -->
    
    <script type="text/javascript">
        var APP_BUILD_DATE = "Wed Jan 25 2017 15:11:40 GMT-0700 (MST)";
        var BUILDER = "kcorkan";
        var CHECKSUM = 2752129514;
    </script>
    
    <script type="text/javascript" src="/apps/x/sdk.js"></script>

    <script type="text/javascript">
        Rally.onReady(function() {
             
/**
 * A link that pops up a version dialog box
 */

Ext.define('Rally.technicalservices.InfoLink',{
    extend: 'Rally.ui.dialog.Dialog',
    alias: 'widget.tsinfolink',
    
    /**
     * @cfg {String} informationHtml
     * Additional text to be displayed on the popup dialog (for exmaple,
     * to add a description of the app's use or functionality)
     */
    informationHtml: null,
    
    /**
     * 
     * cfg {String} title
     * The title for the dialog box
     */
    title: "Build Information",
    
    defaults: { padding: 5, margin: 5 },

    closable: true,
     
    draggable: true,

    autoShow: true,
   
    width: 350,
    
    informationalConfig: null,
    
    items: [{xtype:'container', itemId:'information' }],
    
    initComponent: function() {
        var id = Ext.id(this);
        this.title =  "<span class='icon-help'> </span>" + this.title;
        this.callParent(arguments);
    },
    
    _generateChecksum: function(string){
        var chk = 0x12345678,
            i;
        string = string.replace(/var CHECKSUM = .*;/,"");
        string = string.replace(/var BUILDER = .*;/,"");
        string = string.replace(/\s/g,"");  //Remove all whitespace from the string.
       
        for (i = 0; i < string.length; i++) {
            chk += (string.charCodeAt(i) * i);
        }
   
        return chk;
    },
    
    _checkChecksum: function(container) {
        var deferred = Ext.create('Deft.Deferred');
        var me = this;
        
        Ext.Ajax.request({
            url: document.URL,
            params: {
                id: 1
            },
            success: function (response) {
                text = response.responseText;
                if ( CHECKSUM ) {
                    var stored_checksum = me._generateChecksum(text);
                    if ( CHECKSUM !== stored_checksum ) {
                        deferred.resolve(false);
                        return;
                    }
                }
                deferred.resolve(true);
            }
        });
        
        return deferred.promise;
    },
    
    _addToContainer: function(container){
        var config = Ext.apply({
            xtype:'container',
            height: 200,
            overflowY: true
        }, this.informationalConfig);
        
        container.add(config);
    },
    
    afterRender: function() {
        var app = Rally.getApp();
        
        if ( !Ext.isEmpty( this.informationalConfig ) ) {
            var container = this.down('#information');
            this._addToContainer(container);
            
        }
        
        if (! app.isExternal() ) {
            this._checkChecksum(app).then({
                scope: this,
                success: function(result){
                    if ( !result ) {
                        this.addDocked({
                            xtype:'container',
                            cls: 'build-info',
                            dock: 'bottom',
                            padding: 2,
                            html:'<span class="icon-warning"> </span>Checksums do not match'
                        });
                    }
                },
                failure: function(msg){
                    console.log("oops:",msg);
                }
            });
        } else {
            this.addDocked({
                xtype:'container',
                cls: 'build-info',
                padding: 2,
                dock: 'bottom',
                html:'... Running externally'
            });
        }
        this.callParent(arguments);
    },
    
    beforeRender: function() {
        var me = this;
        this.callParent(arguments);

        if (this.informationHtml) {
            this.addDocked({
                xtype: 'component',
                componentCls: 'intro-panel',
                padding: 2,
                html: this.informationHtml,
                doc: 'top'
            });
        }
        
        this.addDocked({
            xtype:'container',
            cls: 'build-info',
            padding: 2,
            dock:'bottom',
            html:"This app was created by the CA AC Technical Services Team."
        });
        
        if ( APP_BUILD_DATE ) {
            this.addDocked({
                xtype:'container',
                cls: 'build-info',
                padding: 2,
                dock: 'bottom',
                html: Ext.String.format("Build date/time: {0} ({1})",
                    APP_BUILD_DATE,
                    BUILDER)
            });
        }
    }
});

/*
 */
Ext.define('Rally.technicalservices.Logger',{
    constructor: function(config){
        Ext.apply(this,config);
    },
    log: function(args){
        var timestamp = "[ " + Ext.util.Format.date(new Date(), "Y-m-d H:i:s.u") + " ]";
        //var output_args = arguments;
        //output_args.unshift( [ "[ " + timestamp + " ]" ] );
        //output_args = Ext.Array.push(output_args,arguments);
        
        var output_args = [];
        output_args = Ext.Array.push(output_args,[timestamp]);
        output_args = Ext.Array.push(output_args, Ext.Array.slice(arguments,0));

        window.console && console.log.apply(console,output_args);
    }

});


Ext.define('Rally.apps.ppmtimesheet.PPMTimesheetApp', {
    extend: 'Rally.app.App',

    logger: new Rally.technicalservices.Logger(),

    mixins: ['Rally.clientmetrics.ClientMetricsRecordable'],

    appName: 'CA PPM Timesheet Frame',

    config: {
        defaultSettings: {
            ppmHost: null,
            ppmPort: 443,
            ppmRelativePathWithParams: null
        }
    },
    autoScroll: false,
    //timesheetSuffix:  '/pm/integration.html#',
timesheetSuffix:  '',
    launch: function() {

        var server = this.getPPMHost(),
            port = this.getPPMPort();
        
         this.validateConfig(server, port).then({
                success: this.addFrame,
                failure: this.showAppMessage,
                scope: this
        });
    },
    addFrame: function(){
        this.logger.log('addFrame');
        var server = this.getPPMHost(),
            port = this.getPPMPort(),
           relativePath = this.getPPMRelativePath(),
		   
            url = this.buildPPMTimesheetURL(server, port, relativePath);

        try {

            this.add({
                xtype: 'container',
                html: '<div class="secondary-message" style="font-family: ProximaNova,Helvetica,Arial;text-align:center;color:#8a8a8a;font-size:10pt;font-style:italic">Login to CA PPM Timesheet through Agile Central is recommended only when using a private computer. Click on the link below to open CA PPM timesheet in a new tab.'
            });

            var iframe = this.add({
                xtype: 'component',
                itemId: 'ppmIframe',
                autoEl: {
                    tag: 'component',
                    style: 'height: 100%; width: 100%; border: none; content: allow',
                    html:  '<ul><li><a href="' + url + '" target="_blank">Click here to invoke CA PPM Timesheet using SSO.</a></li></ul>'
                }
            });


            var me = this;
            iframe.getEl().dom.onload = function(e){
                me.logger.log('iframe loaded', e, iframe.getEl().dom);
            };
        }
        catch(e){
            Rally.ui.notify.Notifier.showError({message: Ext.String.format("Error loading {0} into iFrame.",url)});
        }

    },
    validateConfig: function(server, port){
        var deferred = Ext.create('Deft.Deferred');

        if (!server){
            deferred.reject("No CA PPM Server and Port is configured.  Please work with an administrator to configure your CA PPM https server.");
        } else {
            //Commented this out due to the chrome issue, as this fails on it.
            //var httpRequest = new XMLHttpRequest(),
            //    suffix = '/ppm/rest/v1/private/userContext',
            //url = this.buildPPMTimesheetURL(server, port);
            //
            //url = url.replace(this.timesheetSuffix, suffix);
            //
            //httpRequest.withCredentials = true;
            //httpRequest.cors = true;
            //httpRequest.onreadystatechange = function() {
            //    console.log('ready', httpRequest.readyState, httpRequest.status);
            //    if (httpRequest.readyState === 4) {
            //        console.log('readystate', httpRequest);
            //        if (httpRequest.status !== 200) {
            //            console.log('Failed', httpRequest.status);
            //            var msg = Ext.String.format('The CA PPM Server and Port provided is not responding as expected. Please verify the configuration in the App Settings.');
            //            deferred.reject(msg);
            //        } else {
                        deferred.resolve();
            //        }
            //    }
            //};
            //httpRequest.open('GET', url);
            //httpRequest.send();
        }

        return deferred;
    },
    buildPPMTimesheetURL: function(server, port, relativePath){
        var httpsIndex = server.indexOf( "https://" );
        //var url = server.startsWith( "https://" ) ? server : Ext.String.format("https://{0}",server);
        var url = httpsIndex == 0 ? server : Ext.String.format("https://{0}",server);
        if (port){
            url = Ext.String.format("{0}:{1}", url, port);
        }
        // if relative path is given then either it's SSO environment or default timesheet suffix is not needed
        if( relativePath ) {
            var urlLastChar = url.charAt(url.length - 1);
            var rPathFirstChar = relativePath.charAt(0);
            //if( !urlLastChar.endsWith("/") && !relativePath.startsWith("/") ) {
            if( urlLastChar != "/" && rPathFirstChar != "/" ) {
              url = url + "/";
            } 
            return url + relativePath;
        } else {
            return url + this.timesheetSuffix;        	
        }
    },
    getPPMHost: function(){
        return this.getSetting('ppmHost') || null;
    },
    getPPMPort: function(){
        return this.getSetting('ppmPort') || null;
    },
    getPPMRelativePath: function(){
        return this.getSetting('ppmRelativePathWithParams') || null;
    },
    showAppMessage: function(msg){
        this.removeAll();
        this.add({
            xtype: 'container',
            html: Ext.String.format('<div class="no-data-container"><div class="secondary-message">{0}</div></div>',msg)
        });
    },
    getSettingsFields: function () {

        return [{
            xtype: 'container',
            html: '<div class="secondary-message" style="font-family: ProximaNovaBold,Helvetica,Arial;text-align:left;color:#B81B10;font-size:12pt;">NOTE:  The CA PPM server must be version 15.2 or above.</div>'
        },{
            name: 'ppmHost',
            xtype: 'rallytextfield',
            width: 400,
            labelWidth: 120,
            labelAlign: 'right',
            fieldLabel: 'CA PPM Host Name (For SSO this will be IDP URL)',
            margin: '10 0 10 0',
            maskRe:  /[a-zA-Z0-9\.\-]/,
            emptyText: 'Please enter a Host name or IP Address...',
            maxLength: 500
        },{
            name: 'ppmPort',
            xtype:'rallynumberfield',
            labelAlign: 'right',
            fieldLabel: 'CA PPM Port (HTTPS)',
            labelWidth: 120,
            emptyText: 443,
            minValue: 0,
            maxValue: 65535,
            allowBlank: true,
            allowDecimals: false,
            allowExponential: false
        },{
            name: 'ppmRelativePathWithParams',
            xtype: 'rallytextfield',
            width: 605,
            labelWidth: 120,
            labelAlign: 'right',
            fieldLabel: 'CA PPM Timesheet Path (For SSO only)',
            margin: '10 0 10 0',
            //maskRe:  /[a-zA-Z0-9\.\-]/,
            //emptyText: 'In SSO environment, enter relative path with parameters of IdP initiated SSO CA PPM URL here and enter host address above ...',
            emptyText: 'In SSO environment, enter relative path with any parameters of IdP initiated SSO CA PPM URL here...',
            maxLength: 500
        }];
    }
});
  
               Rally.launchApp('Rally.apps.ppmtimesheet.PPMTimesheetApp', {
                   name: 'CA PPM Timesheet'
               });
        });
    </script>
    
    <style type="text/css">

.app {
}
.tsinfolink {
    position:absolute;
    right:0px;
    width: 14px;
    height: 14px;
    border-radius: 7px;
    text-align: center;
    color: white;
    background: #C0C0C0;
    border-style: solid;
    border-width: 1px;
    margin-top: 25px;
    margin-right: 5px;
    cursor: pointer;
}

.noScrolling {
    overflow: hidden;
}
    </style>

</head>
<body></body>
</html>

11. Click the Save button
12. Now in your newly created app, click on the Gear icon on the top right (not the one for the page, but the one on the app itself within the page)
13. Fill in the fields as follows:

CA PPM Host Name (For SSO this will be IDP URL):  
https://myapps.microsoft.com/signin/CA PPM Prod/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?RelayState=https%3A%2F%2Fondemand.ca.com%2Ffedsso%3FtargetUrl%3Dhttps%253A%252F%252FPPMSERVER.ondemand.ca.com

CA PPM Port (HTTPS):
443

CA PPM Timehseet Path (For SSO Only):
/pm/#/timesheets

User-added image
14. Click on the Save button

You should now see a link in side your app to launch the PPM Timesheet UI (which will launch the page in a new browser window or tab):

User-added image

Clicking the link will launch a new tab or window which will load the CA PPM New UI Timesheets page.   

NOTE:  The page will NOT have the CA Agile Central frame around it as this will not work when using Azure for SSO for CA PPM.  The page MUST launch in a separate window and be outside of a CA Agile Central frame.