Technical Articles
Custom SAC landing page developed in Analytics Application
Introduction
In this blog post, I would like to share a mock-up of custom SAP Analytics Cloud landing page developed in Analytics Application, based on standard components and APIs, fully automated solution, which does not require any data loads/maintenance.
Overview
SAP Analytics Cloud offers a package called “SAP Analytics Cloud Usage Tracking Content”, which consists of four models and one story.
The models cover the domains of users, files, other objects and activities.
These models open up a new chapter with supporting SAP Analytics Cloud content auditing, but not only.
After several conversations with our SAP Analytics Cloud end-users, many of them claimed that they are bit confused navigating through SAP Analytics Cloud. Of course once you had your head around the environment you get used to it and manage to navigate more swiftly.
It is clear that SAP is investing a lot of efforts in enhancing the user interface by incorporating ‘Catalog’ and soon there will be a new side navigation, a unified top shell bar, and more.
Customization for SAP Analytics Cloud side navigation can be only driven by privileges assigned in Roles, so unfortunately not much freedom here. From the other hand customization around Catalog is much more extended, as you can define (via System/Administration/Catalog) visibility of Catalog tabs and define filters and members, which you can apply on Catalog tiles. Catalog tiles also look rather crispy and functional, so no major objections here.
But what if we would like to completely customize the landing page, like: layout, navigation, mixed with custom content, have a possibility to bookmark it and at the same time – rely on live SAP Analytics Cloud content repository?
And here the combination of SAP Analytics Cloud Usage Tracking Content, together with Analytics Designer and a little bit of out of the box development comes in place.
Approach
We will use SAP__SAC_USAGE_FILES, which is part of SAP Analytics Cloud Usage Tracking Content package.
In Analytics Application we will utilize flow panel and set of panels inside (as a main components of this solution) and apply dynamic navigation and reflect SAP Analytics Cloud content on the tiles.
Effectively we will have 1 master table and several global variables, which will be dynamically fed based on the results from master table and dynamically reflected on the canvas widgets.
Step-by-Step Implementation
Step 1 – Create Layout
Firstly we need to add to Canvas a main container, in which we will put the rest of corresponding widgets. I decided to use Tab Strip.
In order to make application layout flexible and mobile ready, underneath I added Flow Panel.
Now, we need to add number of panels, which will represent our tiles reflecting SAP Analytics Cloud objects – one tile per object. I crafted them in a way to look similar to Catalog tiles, but of course the exact shape and feel can be customized as per our imagination (to some extend).
We need to define/create number of tiles, which will be equal to or greater than maximum number of objects in specific SAP Analytics Cloud repository (specific SAP Analytics Cloud Folder). For this mock-up I created 10 panels, which effectively will represent up to 10 objects in the specific Folder location.
I trust that in a long term Analytics Application will be enhanced by additional feature, which allows to create components on the fly (at run time) – similarly as in SAP Lumira Designer. This could allow us to create number of Panels/tiles on the fly instead of relying on predefined numbers of Panels/tiles in the Canvas.
Add set of Widgets onto each Panel to represent object properties:
- Button (representing Object ID)
- Text (representing Object Description)
- Text (representing object type)
- optionally: Set of images (representing object type)
Create Header Navigation
Header navigation is used to drill back onto specific level. For this sake, we again need to create a Flow Panel, create number of tiles, which will represent maximum number of drill down level. For this mock-up I created 4 panels (4 levels).
Each of these panels should consist of set of Widgets representing navigation level ID and level description
Add auxiliary master table to the canvas (hidden for end user), which will be the main driver for the whole dashboard functionality.
Table consists of the following dimensions/measures:
Rows:
- Parent Folder ID
- Parent Folder
- File Type
- Count of Objects
Filters:
- Measures: Count of Objects
- Parent Folder ID: ‘PUBLIC’
Step 2 – Create set of Script Variables
We need to create the following Script Variables, which will be used to capture navigational properties (each briefly explained)
Tile Container Canvas arrays:
CANVAS_tiles_array (Type: Panel; Array: yes). This is an array that is fed by all the Panels that exists Canvas/Flow_Tile_Container CANVAS_btn_array (Type: Button; Array: yes). This is an array that is fed by all the Buttons that exists Canvas/Flow_Tile_Container CANVAS_txt_array (Type: Text; Array: yes). This is an array that is fed by all the Text used for objects’ description that exists Canvas/Flow_Tile_Container CANVAS_txt_obj_type_array (Type: Text; Array: yes). This is an array that is fed by all the Texts used for objects’ type that exists Canvas/Flow_Tile_Container
Optionally:
CANVAS_img_folder_array (Type: Image; Array: yes). This is an array that is fed by all the Images that exists Canvas/Flow_Tile_Container CANVAS_img_model_array (Type: Image; Array: yes). This is an array that is fed by all the Images that exists Canvas/Flow_Tile_Container CANVAS_img_story_array (Type: Image; Array: yes). This is an array that is fed by all the Images that exists Canvas/Flow_Tile_Container CANVAS_img_url_array (Type: Image; Array: yes). This is an array that is fed by all the Images that exists Canvas/Flow_Tile_Container
Header Navigation Canvas arrays:
CANVAS_LEVEL_btn_array (Type: Button; Array: yes). This is an array that is fed by all the Buttons that exists Canvas/Flow_Header_Navigation CANVAS_LEVEL_btn_array (Type: Text; Array: yes). This is an array that is fed by all the Texts that exists Canvas/Flow_Header_Navigation
Tile Container arrays:
Tiles _OBJ_TYPE_DESC_array (Type: String; Array: yes). This is an array that is fed by SAP Analytics Cloud objects/members Types Tiles _PARENT_OBJ_DESC_array (Type: String; Array: yes). This is an array that is fed by SAP Analytics Cloud objects/members Description Tiles _PARENT_OBJ_ID_array (Type: String; Array: yes). This is an array that is fed by SAP Analytics Cloud objects/members ID
Tile Container selection variables:
Tile_selected_OBJ_TYPE_DESC_str (Type: String; Array: no). This is a string that is populated by SAP Analytics Cloud object Type selection Tile_selected_PARENT_OBJ_DESC_str (Type: String; Array: no). This is a string that is populated by SAP Analytics Cloud object Description selection Tile_selected_PARENT_OBJ_ID_str (Type: String; Array: no). This is a string that is populated by SAP Analytics Cloud object ID selection
Navigation level variable:
LEVEL (Type: Integer; Array: no). This is a integer, which indicates current navigational level of SAP Analytics Cloud folder repository
Optional variables for enhanced features:
AppUser (Type: String; Array: no). This is a string is used to capture current SAP Analytics Cloud user
Step 3 – Create set of Script Objects:
CANVAS_feed_arrays() This function is used for feeding CANVAS global variables
//arrays for Canvas tile containder objects
CANVAS_tiles_array = [TILE_1, TILE_2, TILE_3, TILE_4, TILE_5, TILE_6, TILE_7, TILE_8, TILE_9, TILE_10];
CANVAS_txt_array = [TXT_1, TXT_2, TXT_3, TXT_4, TXT_5, TXT_6, TXT_7, TXT_8, TXT_9, TXT_10];
CANVAS_btn_array = [BTN_1, BTN_2, BTN_3, BTN_4, BTN_5, BTN_6, BTN_7, BTN_8, BTN_9, BTN_10];
CANVAS_txt_obj_type_array = [TXT_OBJ_TYPE_1, TXT_OBJ_TYPE_2, TXT_OBJ_TYPE_3, TXT_OBJ_TYPE_4, TXT_OBJ_TYPE_5, TXT_OBJ_TYPE_6, TXT_OBJ_TYPE_7, TXT_OBJ_TYPE_8, TXT_OBJ_TYPE_9, TXT_OBJ_TYPE_10];
CANVAS_img_folder_array = [FOLDER_1, FOLDER_2, FOLDER_3, FOLDER_4, FOLDER_5, FOLDER_6, FOLDER_7, FOLDER_8, FOLDER_9, FOLDER_10];
CANVAS_img_story_array = [STORY_1, STORY_2, STORY_3, STORY_4, STORY_5, STORY_6, STORY_7, STORY_8, STORY_9, STORY_10];
CANVAS_img_model_array = [MODEL_1, MODEL_2, MODEL_3, MODEL_4, MODEL_5, MODEL_6, MODEL_7, MODEL_8, MODEL_9, MODEL_10];
CANVAS_img_url_array = [URL_1, URL_2, URL_3, URL_4, URL_5, URL_6, URL_7, URL_8, URL_9, URL_10];
//array for Canvas header LEVEL buttons
CANVAS_LEVEL_btn_array = [BTN_LVL1, BTN_LVL2, BTN_LVL3, BTN_LVL4];
CANVAS_LEVEL_txt_array = [TXT_LVL1, TXT_LVL2, TXT_LVL3, TXT_LVL4];
SetHeaders() This function is used to manage labels and visibility of Header_Navigation objects depends on the drill down level
if (LEVEL === 0)
{
BTN_LVL1.setText("Public");
TXT_LVL1.applyText("Public");
BTN_LVL2.setVisible(false);
TXT_LVL2.setVisible(false);
BTN_LVL3.setVisible(false);
TXT_LVL3.setVisible(false);
BTN_LVL4.setVisible(false);
TXT_LVL4.setVisible(false);
}
else if (LEVEL === 1)
{
BTN_LVL2.setText(Tile_selected_PARENT_OBJ_ID_str);
TXT_LVL2.applyText(Tile_selected_PARENT_OBJ_DESC_str);
BTN_LVL2.setVisible(true);
TXT_LVL2.setVisible(true);
BTN_LVL3.setVisible(false);
TXT_LVL3.setVisible(false);
BTN_LVL4.setVisible(false);
TXT_LVL4.setVisible(false);
}
else if (LEVEL === 2)
{
BTN_LVL3.setText(Tile_selected_PARENT_OBJ_ID_str);
TXT_LVL3.applyText(Tile_selected_PARENT_OBJ_DESC_str);
BTN_LVL3.setVisible(true);
TXT_LVL3.setVisible(true);
BTN_LVL4.setVisible(false);
TXT_LVL4.setVisible(false);
}
else if (LEVEL === 3)
{
BTN_LVL4.setText(Tile_selected_PARENT_OBJ_ID_str);
TXT_LVL4.applyText(Tile_selected_PARENT_OBJ_DESC_str);
BTN_LVL4.setVisible(true);
TXT_LVL4.setVisible(true);
}
RefreshTiles() This function is used for feeding Script Variables (arrays members) dedicated for Tiles
//clear filter on tbl_MASTER
tbl_MASTER.getDataSource().removeDimensionFilter("PARENT_FOLDER_ID");
tbl_MASTER.getDataSource().setDimensionFilter("PARENT_FOLDER_ID",Tile_selected_PARENT_OBJ_ID_str);
//get object ID and Description
var selections = tbl_MASTER.getDataSource().getDataSelections();
for (var i = 0; i < selections.length; i++) {
var member = tbl_MASTER.getDataSource().getResultMember("PARENT_FOLDER", selections[i]);
Tiles_PARENT_OBJ_ID_array.push(member.id);
Tiles_PARENT_OBJ_DESC_array.push(member.description);
}
//get file type
selections = tbl_MASTER.getDataSource().getDataSelections();
for (i = 0; i < selections.length; i++) {
member = tbl_MASTER.getDataSource().getResultMember("FILE_TYPE", selections[i]);
{
Tiles_OBJ_TYPE_DESC_array.push(member.description);
}
}
GlobalScripts_1.DefineTiles();
DefineTiles() This function is used to reflect Script Variables (arrays members) on the Canvas objects depends on the Tiles Script Variables (arrays members)
for (var i=0; i<CANVAS_tiles_array.length; i++)
{
if (Tiles_PARENT_OBJ_DESC_array.length === 0)
{
CANVAS_tiles_array[i].setVisible(false);
}
else
{
CANVAS_tiles_array[i].setVisible(true);
CANVAS_txt_array[i].applyText(Tiles_PARENT_OBJ_DESC_array.shift());
CANVAS_btn_array[i].setText(Tiles_PARENT_OBJ_ID_array.shift());
Tile_selected_OBJ_TYPE_DESC_str = Tiles_OBJ_TYPE_DESC_array.shift();
CANVAS_txt_obj_type_array[i].applyText(Tile_selected_OBJ_TYPE_DESC_str);
if (Tile_selected_OBJ_TYPE_DESC_str === "Folder")
{
CANVAS_img_folder_array[i].setVisible(true);
CANVAS_img_story_array[i].setVisible(false);
CANVAS_img_model_array[i].setVisible(false);
CANVAS_img_url_array[i].setVisible(false);
}
else if (Tile_selected_OBJ_TYPE_DESC_str === "Story")
{
CANVAS_img_folder_array[i].setVisible(false);
CANVAS_img_story_array[i].setVisible(true);
CANVAS_img_model_array[i].setVisible(false);
CANVAS_img_url_array[i].setVisible(false);
}
else if (Tile_selected_OBJ_TYPE_DESC_str === "Model" || Tile_selected_OBJ_TYPE_DESC_str === "Dataset")
{
CANVAS_img_folder_array[i].setVisible(false);
CANVAS_img_story_array[i].setVisible(false);
CANVAS_img_model_array[i].setVisible(true);
CANVAS_img_url_array[i].setVisible(false);
}
else if (Tile_selected_OBJ_TYPE_DESC_str === "Link" || Tile_selected_OBJ_TYPE_DESC_str === "Unknown")
{
CANVAS_img_folder_array[i].setVisible(false);
CANVAS_img_story_array[i].setVisible(false);
CANVAS_img_model_array[i].setVisible(false);
CANVAS_img_url_array[i].setVisible(true);
}
}
}
OpenObject() This function is used to navigate over the repository depends on object type that user clicked (either open up Object URL or drill down to sub-folder)
Flow_Tile_Container.setVisible(false);
//Application.showBusyIndicator("loading");
//if it's an objects - open up in separate tab
if (Tile_selected_OBJ_TYPE_DESC_str === "Story")
{
var URLPrefix = "https://<HOST>:<PORT>/sap/fpa/ui/tenants/d27b0/bo/story/";
var URL_content = Tile_selected_PARENT_OBJ_ID_str;
var Sufix = "?mode=embed";
var URL = URLPrefix + URL_content + Sufix;
NavigationUtils.openUrl(URL);
}
else if (Tile_selected_OBJ_TYPE_DESC_str === "Model")
{
URLPrefix = "https://<HOST>:<PORT>/sap/fpa/ui/tenants/d27b0#view_id=model;model_id=";
URL_content = Tile_selected_PARENT_OBJ_ID_str;
URL = URLPrefix + URL_content;
NavigationUtils.openUrl(URL);
}
else if (Tile_selected_OBJ_TYPE_DESC_str === "Point of Interest")
{
//do nothing
}
else if (Tile_selected_OBJ_TYPE_DESC_str === "Dataset")
{
URLPrefix = "https://<HOST>:<PORT>/sap/fpa/ui/tenants/d27b0#view_id=ds-datasethome;datasetId=";
URL_content = Tile_selected_PARENT_OBJ_ID_str;
URL = URLPrefix + URL_content;
NavigationUtils.openUrl(URL);
}
else if (Tile_selected_OBJ_TYPE_DESC_str === "Link")
{
//do nothing
}
else if (Tile_selected_OBJ_TYPE_DESC_str === "Unknown")
{
//do nothing
}
else if (Tile_selected_OBJ_TYPE_DESC_str === "File")
{
URLPrefix = "https://<HOST>:<PORT>/sap/fpa/ui/tenants/d27b0/bo/story/";
URL_content = Tile_selected_PARENT_OBJ_ID_str;
Sufix = "?mode=embed";
URL = URLPrefix + URL_content + Sufix;
NavigationUtils.openUrl(URL);
}
else if (Tile_selected_OBJ_TYPE_DESC_str === "Folder")
{
GlobalScripts_1.RefreshTiles();
//increase level value (then it is driven by header levels)
LEVEL = LEVEL+1;
GlobalScripts_1.SetHeaders();
}
//Application.hideBusyIndicator();
Flow_Tile_Container.setVisible(true);
Step 4 – Set Application and functions behind canvas objects
onInitialization()
//get user
AppUser = Application.getUserInfo().id;
SACUser.applyText(AppUser);
//sliding menu
slidein.setVisible(false);
slideout.setVisible(true);
slidein_header.setVisible(false);
slideout_header.setVisible(true);
// Get members from PARENT_FOLDER dimensions (already prefiltered in the table PARENT_ID = PUBLIC)
var selections = tbl_MASTER.getDataSource().getDataSelections();
for (var i = 0; i < selections.length; i++) {
var member = tbl_MASTER.getDataSource().getResultMember("PARENT_FOLDER", selections[i]);
Tiles_PARENT_OBJ_ID_array.push(member.id);
Tiles_PARENT_OBJ_DESC_array.push(member.description);
}
//get file type
selections = tbl_MASTER.getDataSource().getDataSelections();
for (i = 0; i < selections.length; i++) {
member = tbl_MASTER.getDataSource().getResultMember("FILE_TYPE", selections[i]);
{
Tiles_OBJ_TYPE_DESC_array.push(member.description);
}
}
//show results for both arrays in console
console.log(Tiles_PARENT_OBJ_DESC_array);
console.log(Tiles_PARENT_OBJ_ID_array);
console.log(Tiles_OBJ_TYPE_DESC_array);
//feed arrays dedicated for the objects in the CANVAS:
GlobalScripts_1.CANVAS_feed_arrays();
//define tiles:
GlobalScripts_1.DefineTiles();
//define navigation headers
GlobalScripts_1.SetHeaders();
onClick for each button (individually) underneath Flow_Tile_Container BTN_1
Tile_selected_PARENT_OBJ_ID_str = BTN_1.getText(); //to be used as a filter value
Tile_selected_PARENT_OBJ_DESC_str = TXT_1.getPlainText(); //to be used for header levels
Tile_selected_OBJ_TYPE_DESC_str = TXT_OBJ_TYPE_1.getPlainText(); //to be used to determine object type
GlobalScripts_1.OpenObject();
BTN_2
Tile_selected_PARENT_OBJ_ID_str = BTN_2.getText(); //to be used as a filter value
Tile_selected_PARENT_OBJ_DESC_str = TXT_2.getPlainText(); //to be used for header levels
Tile_selected_OBJ_TYPE_DESC_str = TXT_OBJ_TYPE_2.getPlainText(); //to be used to determine object type
GlobalScripts_1.OpenObject();
etc.
onClick for each button (individually) underneath Flow_Header_Navigation
BTN_LVL1
LEVEL = 0;
Tile_selected_PARENT_OBJ_ID_str = "PUBLIC";
Tile_selected_PARENT_OBJ_DESC_str = "PUBLIC";
GlobalScripts_1.RefreshTiles();
GlobalScripts_1.SetHeaders();
BTN_LVL2
LEVEL = 1;
Tile_selected_PARENT_OBJ_ID_str = BTN_LVL2.getText(); //to be used as a filter value
Tile_selected_PARENT_OBJ_DESC_str = TXT_LVL2.getPlainText(); //to be used for header levels
GlobalScripts_1.RefreshTiles();
GlobalScripts_1.SetHeaders();
etc.
Step 5 – Optionally
Since Analytics Application gives you a lot of flexibility around different widgets available out of the box, you can enhance your landing page by extra functions, which are limited almost only to your imagination 😊
Couple of ideas:
- Simplify the logic behind arrays by replacing them by 2 dimensional arrays. Since Analytics Application supports Java subset, it does not come out of the box, but it is feasible by a workaround.
- If we had 2 dimensional arrays, we could add custom sorting for a members inside the array (plus tiles sorting feature)
- Add fancy sliding menu with additional buttons, such as ‘Create Story’ (or rather open up certain blank Story Template, which could be potentially correlated with User properties, e.g. team that user belongs to)
- Set/Get personal or global Bookmarks
- Embed SAP Analytics Cloud Classic website in separate TabStrip
- Create separate Navigation for ‘My Files”
- Create a dynamic Welcome message/memo for all users (or several memos – only relevant – visibility driven by Team(s) where user belongs to (you can utilize SAP__SAC_USAGE_USERS Model)
Step 6 – Enjoy the working scenario
Step 7 – Example
SAP Analytics Cloud Analytics Application Export available in the following location:
- Download the “FPA_EXPORT_T_A_SAC_LANDING_PAGE.tgz“
- Import the application onto your SAP Analytics Cloud environment (DeploymentàImport)
- If you did not do it before: Import “SAP Analytics Cloud Usage Tracking Content” Package: https://saphanajourney.com/sap-analytics-cloud/learning-article/sap-analytics-cloud-usage-tracking/
- Navigate to location where you imported SAC_LANDING_PAGE Analytics Application and open it in Edit mode
- In Canvas: search for tbl_MASTER and remap it with your model (“SAP__SAC_USAGE_FILES”, which is part of “SAP Analytics Cloud Usage Tracking Content” Package). After remapping: re-define Rows and Filters for the table (as already described in this blog post)
- Modify function: OpenObjects() by replacing generic URLPrefix: https://<HOST>:<PORT> by your SAP Analytics Cloud tenant Host and Port
Remarks
One drawback of this solution, that I could not overcome, is no possibility to show SAP Analytics Cloud content based on sharing properties defined for folders/objects.
This is due to lack of availability of sharing properties per object in SAP__SAC_USAGE_FILES Model.
There is a way to get information around Sharing activities from SAP__SAC_USAGE_ACTIVITIES Model, but how to correlate it with SAP__SAC_USAGE_FILES requires further investigation.
Thank you for sharing this solution.
Hi Kris,
This is exactly what I am looking for and I was also thinking of using the audit story.
Another thing I am looking for is, if there is a way to set these tabs as a carousel style? Like Sales, Finance, Operational instead of tabs? or mouse over Sales, Finc, etc and show the relevant landing page - se attached image. I am not a java coder so if you can let me know if this is doable in app designer in SAC then I can do some research.
Thank you so much for this great solution.
Hello,
Could you update .tgz file? Current version is 'old'. Thanks.