Technical Articles
Control SAP Analytics Cloud, Analytics Designer App Remotely with Mobile Phone
I would like to share how to control the SAP Analytics Cloud, Analytics Designer app remotely from the mobile device. Imagine you have a big display board showing the analytical apps and people can interact with it with their mobile phones. Or during presentation, you can control remotely the analytical apps with your mobile phone or even with your voice command. That sounds cool isn’t it!
Scenarios
In this tutorial, we are going to build a simple Analytics Designer app that is showing the gross margin per location or per product with the dummy data from BestRunJuice company.
We will create two charts: Indicator: Bullet chart and Comparison: Combination Column & Line. We will also add the QR code information for the user to interact with the charts via a mobile device.
Once the QR code is generated, user uses their mobile device to scan the QR code on the app to open the control page and then interact with the control page on the mobile phone to remotely control the app on the big screen
On the mobile phone screen, we will create two buttons: Location and Product for user to filter the chart based on these two inputs.
Components and High Level Diagram
In order to build, we need to have:
Based on the above diagram, there are four modules as follow:
- Receiver
It is an HTML page that is embedded in the analytics app via Webpage Widget to generate the QR code and pass the command actions from the mobile phone to the analytics app via Window.postMessage().In the receiver.html, it will try to make a socket connection to the HANA XSA App (NodeJS server) and once it is successfully connected, it will generate the QR code image with the input parameter: “https://HANA_XSA_APP_URL/controller.html?uid=” + socket.id and sends the command “connect” to the analytics app. socket.id acts like a token. A user with a token can control the app.If there is “req” event from the socket connection, it will pass the command action to the analytics app. The command actions are “Location” and “Product”.If there is “disconnect” event from the socket connection, it will delete the QR code image and sends the command “disconnect” to the analytics app.User scan the QR code and open remote url (Controller).
- Controller
It is an HTML page that runs on your mobile phone after you scan the QR code. It has two buttons “Product” and “Location” for user to filter the chart.In the controller.html, it will try to make a socket connection to the HANA XSA App (NodeJS server).When user pressed the button, it will emit the following socket message with event “cmd_req” to the NodeJS server:{message : "Location"|"Product", uid: uid}
with the parameter uid is the unique socket id (token).
- NodeJS Server.js
It is a NodeJS service running on SAP HANA XSA to:
– Receive commands from controller.html
– Pass the commands to the respective client (receiver.html) based on the socket.id (token). - Analytics App
In the Analytics application’s onPostMessageReceived event, it parses message to get command and perform some logic to filter the charts based on the “Location” and “Product” criteria.It is also listen to the socket event “connect” and “disconnect“.If the connection to the NodeJS server is down, the red little icon will be shown and the QR code image will be deleted. Once the connection is restored, the QR code image will be regenerated and the red little icon will be dissapeared.
Okay, enough with theory, now let’s get started to build the app with these steps.
Steps
- SAP HANA XSA Setup
– Create NodeJS Module
– Create Web Module
– Update mta.yaml and xs-security.json
– Run NodeJS and Web Module - Create SAP Analytics Cloud, Analytics Designer App
SAP HANA XSA Setup
Logon to SAP Web IDE for HANA 2 XS Advanced (XSA) and create a new project from the template.
- Select SAP Cloud Platform Business Application and click Next.
- Enter the project name and click Next.
- At this page, just click Next.
- Set Service(srv) and Database(db) to Not included. Click Next.
- Click Finish to complete the setup.
- You will see the project structure is created in the workplace.
Create the NodeJS app in SAP HANA XS Advanced Module with NodeJS and HTML module.
Create NodeJS Module
- Right click on the project folder and select New > Node.js Module.
- Enter the module name srv and click Next.
- At this page, click Next.
- Click Finish to complete.
- You will see the srv folder in the project folder.
- Open server.js in srv folder and insert the following codes:
/*eslint no-console: 0, no-unused-vars: 0, no-undef:0, no-process-exit:0*/ /*eslint-env node, es6 */ "use strict"; const port = process.env.PORT || 3000; const server = require("http").createServer(); const cds = require("@sap/cds"); //Initialize Express App for XSA UAA and HDBEXT Middleware const xsenv = require("@sap/xsenv"); const passport = require("passport"); const xssec = require("@sap/xssec"); const xsHDBConn = require("@sap/hdbext"); const express = require("express"); global.__base = __dirname + "/"; //logging var logging = require("@sap/logging"); var appContext = logging.createAppContext(); //Initialize Express App for XS UAA and HDBEXT Middleware var app = express(); //Compression app.use(require("compression")({ threshold: "1b" })); //Helmet for Security Policy Headers const helmet = require("helmet"); // ... app.use(helmet()); app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "sapui5.hana.ondemand.com"], scriptSrc: ["'self'", "sapui5.hana.ondemand.com"] } })); // Sets "Referrer-Policy: no-referrer". app.use(helmet.referrerPolicy({ policy: "no-referrer" })); //Start the Server server.on("request", app); // use socket.io var io = require("socket.io").listen(server); // define interactions with client io.sockets.on("connection", function (socket) { //listen on new_message from GUI socket.on("cmd_req", (data) => { console.log(data.message + ":" + data.uid); if(data.message === "Product") { socket.broadcast.to(data.uid).emit("req", {message : "Product"}); } if(data.message === "Location") { socket.broadcast.to(data.uid).emit("req", {message : "Location"}); } }); }); server.listen(port, function () { console.info(`HTTP Server: ${server.address().port}`); });
- Open package.json in srv folder and insert the following codes:
{ "name": "serve", "description": "Generated from ../package.json, do not change!", "version": "1.0.0", "dependencies": { "@sap/cds": "^3.10.0", "express": "^4.17.1", "@sap/xssec": "^2.1.17", "@sap/xsenv": "^2.0.0", "hdb": "^0.17.0", "@sap/hdbext": "^6.0.0", "@sap/hana-client": "^2.4.139", "@sap/textbundle": "latest", "@sap/logging": "^5.0.1", "@sap/audit-logging": "^3.0.0", "nodemailer": "^6.2.1", "passport": "~0.4.0", "async": "^3.0.1", "ws": "^7.0.0", "accept-language-parser": "latest", "node-xlsx": "^0.15.0", "node-zip": "~1.1.1", "xmldoc": "~1.1.2", "winston": "^3.2.1", "body-parser": "^1.19.0", "elementtree": "latest", "then-request": "latest", "compression": "~1.7", "helmet": "^3.18.0", "socket.io": "^2.2.0", "socket.io-client": "^2.2.0" }, "engines": { "node": "^8.9", "npm": "^6" }, "devDependencies": {}, "scripts": { "postinstall": "cds build/all --project .. --clean", "start": "node server.js" }, "i18n": { "folders": [ "_i18n" ] }, "cds": { "data": { "driver": "hana" } } }
Create Web Module
- Right click on the project module and select New > Basic HTML5 Module.
- Give a module name web. Click Next.
- Click Finish to complete.
- You will see the web folder in the project folder.
- Go to web > resources folder and create two files: controller.html and receiver.html. You can remove index.html.
- Insert the following codes for controller.html.
<html> <title>SAP Cloud Analytics</title> <body> <style type="text/css"> strong { font-weight: bold; } .button { background-color: #f2f2f2; background-image: linear-gradient(to bottom, #f2f2f2, #f2f2f2); border: 1px solid #bfbfbf; box-shadow: inset 0 1px 0 white, inset 0 -1px 0 #d9d9d9, inset 0 0 0 1px #f2f2f2, 0 2px 4px rgba(0, 0, 0, 0.2); color: #8c8c8c; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); border-radius: 9px; cursor: pointer; display: inline-block; font-family: Verdana, sans-serif; font-size: 52px; font-weight: 350; line-height: 100px; padding: 19px 116px 19px; margin: 16px 0 30 16px; transition: all 20ms ease-out; vertical-align: top; } .button:hover, .button:focus { background: #f2f2f2; border-color: #8c8c8c; box-shadow: inset 0 1px 0 white, inset 0 -1px 0 #d9d9d9, inset 0 0 0 1px #f2f2f2; } .button:active { background: #f2f2f2; box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.2); } .button .fa { color: #bfbfbf; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); } .button-blue { background-color: #42b0e3; background-image: linear-gradient(to bottom, #42b0e3, #2ba9e3); border: 1px solid #107db0; box-shadow: inset 0 1px 0 #7cd4fc, inset 0 -1px 0 #2696c9, inset 0 0 0 1px #59b7e3, 0 2px 4px rgba(0, 0, 0, 0.2); color: white; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } .button-blue:hover, .button-blue:focus { background: #2ba9e3; border-color: #004c6f; box-shadow: inset 0 1px 0 #7cd4fc, inset 0 -1px 0 #2696c9, inset 0 0 0 1px #59b7e3; } .button-blue:active { background: #2ba9e3; box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.2); } .button-blue .fa { color: #107db0; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); } .button-red { background-color: #ed6d64; background-image: linear-gradient(to bottom, #ed6d64, #ed574c); border: 1px solid #ba3329; box-shadow: inset 0 1px 0 #ffb0aa, inset 0 -1px 0 #d44d44, inset 0 0 0 1px #ed837b, 0 2px 4px rgba(0, 0, 0, 0.2); color: white; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } .button-red:hover, .button-red:focus { background: #ed574c; border-color: #870c03; box-shadow: inset 0 1px 0 #ffb0aa, inset 0 -1px 0 #d44d44, inset 0 0 0 1px #ed837b; } .button-red:active { background: #ed574c; box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.2); } .button-red .fa { color: #ba3329; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.3); } .button .fa { float: left; font-size: 14px; line-height: 20px; margin: -1px 8px 0 -4px; vertical-align: top; } </style> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> <script src="/socket.io/socket.io.js"></script> <script> function getUrlVars() { var vars = {}; var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) { vars[key] = value; }); return vars; } function getUrlParam(parameter, defaultvalue){ var urlparameter = defaultvalue; if(window.location.href.indexOf(parameter) > -1){ urlparameter = getUrlVars()[parameter]; } return urlparameter; } var uid = getUrlParam('uid',''); var socket = io({transports: ['websocket'], upgrade: false}); var socket = io.connect(); socket.on('connect', function () { console.log("socket connected"); }); function myFunction_Product() { socket.emit('cmd_req', {message : "Product", uid: uid}); } function myFunction_Location() { socket.emit('cmd_req', {message : "Location", uid: uid}); } </script> <a class="button button-red" onclick="myFunction_Location();"> <i class="fa fa-times"></i> <strong>Location</strong> </a> <br> <a class="button button-blue" onclick="myFunction_Product()"> <i class="fa fa-globe"></i> <strong>Product</strong> </a> </body> </html>
- Insert the following codes for receiver.html.
<!DOCTYPE HTML> <html> <head> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv='Content-Type' content='text/html;charset=UTF-8' /> <title>SAP Analytics Cloud</title> <script src="/socket.io/socket.io.js"></script> <script type="text/javascript" src="qrcode.min.js"></script> <script> var socket = io.connect(); socket.on('connect', function () { console.log("socket connected:" + socket.id); var qrcode = new QRCode(document.getElementById("qrcode"), { width : 250, height : 250 }); qrcode.makeCode("https://<YOUR_HANA_XSA_APP_URL>/controller.html?uid=" + socket.id); window.parent.postMessage('connect', '*'); }); //Listen for req event from the remote socket.on('req', function(data){ console.log('Message from remote: ' + data.message); window.parent.postMessage(data.message, '*'); }); //Listen for disconnect even from the server socket.on('disconnect', function(){ console.log('disconnect'); window.parent.postMessage('disconnect', '*'); var list = document.getElementById("qrcode"); var child = list.lastElementChild; while (child) { list.removeChild(child); child = list.lastElementChild; } }); </script> </head> <body> <div id="qrcode" style="width:100px; height:100px; margin-top:15px;"></div> </body> </html>
- Get qrcode.min.js from https://davidshimjs.github.io/qrcodejs/ and copy to web > resources folder.
- Open xs-app.json in web folder and insert the following codes:
{ "welcomeFile": "index.html", "authenticationMethod": "none", "routes": [{ "source": "/socket.io(.*)", "destination": "srv_api", "csrfProtection": true, "authenticationType": "none" }] }
Update mta.yaml and xs-security.json
- Open mta.yaml and insert the following codes:
ID: zsacanalytics _schema-version: "2.1" version: 0.0.1 modules: - name: srv type: nodejs path: srv parameters: memory: 512M disk-quota: 256M provides: - name: srv_api properties: url: '${default-url}' requires: - name: zsacanalytics-uaa - name: web type: html5 path: web requires: - name: zsacanalytics-uaa - name: srv_api group: destinations properties: name: srv_api url: '~{url}' forwardAuthToken: true resources: - name: zsacanalytics-uaa type: com.sap.xs.uaa-space parameters: config-path: ./xs-security.json
- Create file xs-security.json in the root of project folder and insert the following codes:
{ "xsappname": "zearnpfe", "scopes": [{ "name": "$XSAPPNAME.Display", "description": "display" }, { "name": "$XSAPPNAME.Create", "description": "create" }, { "name": "$XSAPPNAME.Edit", "description": "edit" }, { "name": "$XSAPPNAME.Delete", "description": "delete" }, { "name": "$XSAPPNAME.DataGenerator", "description": "data generator" }, { "name": "xs_authorization.read", "description": "Read authorization information from UAA" }, { "name": "xs_authorization.write", "description": "Write authorization information to UAA" }, { "name": "$XSAPPNAME.ODATASERVICEUSER", "description": "Enter" }, { "name": "$XSAPPNAME.ODATASERVICEADMIN", "description": "Enter" }], "attributes": [{ "name": "client", "description": "Session Client", "valueType": "int" }, { "name": "country", "description": "country", "valueType": "s" }], "role-templates": [{ "name": "Viewer", "description": "View all records", "scope-references": [ "$XSAPPNAME.Display" ], "attribute-references": [ "client", "country" ] }, { "name": "Editor", "description": "Edit and Delete records", "scope-references": [ "$XSAPPNAME.Create", "$XSAPPNAME.Edit", "$XSAPPNAME.Delete", "$XSAPPNAME.Display", "$XSAPPNAME.DataGenerator", "$XSAPPNAME.ODATASERVICEUSER", "$XSAPPNAME.ODATASERVICEADMIN" ], "attribute-references": [ "client" ] }] }
- All codes can be found on my Git.
Run the NodeJS and Web Module
Run both NodeJS and Web Module.
Get the URL of the receiver.html. We will embed this URL in Analytics App web widget later.
Create SAP Analytics Cloud, Analytics Designer App
Create the Analytics Designer App with two charts:
And add the web page widget to embed the receiver.html.
If you have any issue when embedding the web page, like the page is not loaded, refer to this blog.
On the onInitialization and onPostMessageReceived, insert the codes as per the below screnshots.
Don’t forget to save it. And now you can run the app.
You can try to experiment with any other charts and create some logic behind them. Let me know in the comment if you have any questions.
Reference
Hello Ferry,
Greate content!
is it possible to develop an application similar to this without sap Hana xsa? by purely using a js framework?
Regards,
Aditya Mishra.
Hello Aditya,
Yes that's also possible because the code in HANA XSA is also using JS.
Regards,
Ferry