Skip to Content
Technical Articles
Author's profile photo Steffen Froehlich

Custom Tiles with Cloud Foundry Portal – Cloud Platform

I like to write some notes down, from my findings how to implement custom tiles with in Portal service on Cloud Foundry (SCP / Cloud Platform). Unfortunately, the current documentation is limited and doesn’t show a good end-to-end sample. I only could get it running with a lot of trial and error.

 

Updates

03/27/2020 – added end-to-end sample with one Portal, two custom tiles, one basic UI5 app as target and CAP based service as backend for the charts: https://github.com/citoki/portal-custom-tile

 

This is the result. You’ll get a tile, where you can put in your controls of choice. In my sample below, I use two ComparisonMicroChart controls in the left tile and in the right one two HarveyBallMicroChart controls. For the first tile I used a JS-View and for the second a XML-View, just for demonstration. You can choose what fits better to your coding style.

Contents

  • Create a Starter Project
  • Create a Tile App
  • Tile Content
  • Reading Data Continuously
  • Parameterise Custom Tile via FLP Config
  • Code – End-To-End Sample

Create a Starter Project

As a starting point for a new FLP project I like to generate the necessary artefacts by the WebIDE. You can follow the steps described by Iris Sasson in her detailed blog: SAP Fiori launchpad site with portal service on SAP Cloud Platform

One thing which is crucial to get a minimal project running on CF (Cloud Foundry), is that the custom tile points to an existing UI5 app. That is the target app which will be defined in business app file baCustomTile.json later in this blog.

In my case, I called the target app like this: tutorial.app.one.app1

Create a Tile App

Now let’s jump into the code!

For the new custom tile we need a separate UI5 app. There are a lot of ways to scaffold an UI5 app. One thing I can recommend is the Yo-plugin by Marius Obert. He started developing the Yo plugin Generator-Easy-UI5. With that plugin you’ll be fast on the track.

The only thing you need to add in the UI5 app, is the FLP configuration This tells the Portal service that the app should be handled as a Tile with a specific tileSize of 1×1.

"sap.flp": {
  "type": "tile",
  "tileSize": "1x1"
},

tileSize currently only supports 1x1 (square tile) and 1x2 (wide rectangle tile).

Fiori Launchpad Configuration

Below you can see the structure we need in the end.

flp/portal-site/CommonDataModel.json
flp/portal-site/business-apps
└── baCustomTile.json
flp/portal-site/i18n
└── i18n.properties

The configuration for the Launchpad (FLP) is done via the flp/portal-site/CommonDataModel.json file. In config path “payload.catalogs.payload.viz add following object to the viz array:

File: CommonDataModel.json

viz: [{
  "id": "tutorial.app.one.app1", // has to be equal to 'appId' wihtin groups
  "vizId": "customTile-displayToCustom" // visualization from business app configuration
}]

This tells the FLP runtime, that there is a special visualisation for  app1 available.

In the same file (CommonDataModel.json), we need to set the specific visualisation for the tile. That is done in the object path “payload.groups.payload“. Just add the following code there:

File: CommonDataModel.json

"viz": [{
  "id": "tutorial.app.one.app1-0-1573035031268",
  "appId": "tutorial.app.one.app1",
  "vizId": "customTile-displayToCustom" // visualisation ID from business app configuration
}]

The configuration and the target mapping, when clicking on the custom tile has to be made in a specific folder flp/portal-site/business-apps. Just create a JSON file (in my example: baCustomTile.json) with following code:

File: baCustomTile.json

{
    "_version": "3.0.0",
    "identification": {
        "id": "tutorial.app.one.app1", // app which should be opened by the custom tile
        "entityType": "businessapp",
        "i18n": "i18n/i18n.properties"
    },
    "payload": {
        "visualizations": { // multiple visualisations are possible to be defined here
            "customTile-displayToCustom": { // one sample of visualization for our 'app1'
                "vizType": "project.namespace.customTile", // type has to be set to the app name of our customTile
                "vizConfig": {
                    "sap.app": {
                        "title": "{{notifList.tile.title}}",
                        "subTitle": "{{notifList.tile.subtitle}}"
                    },
                    "sap.flp": {
                        "target": {
                            "inboundId": "data-display" // inbound config for the target app 'app1'
                        }
                    }
                }
            }
        }
    }
}

 

And that’s it. Just deploy the FLP like normal and the custom tile should appear right there.

Tile Content ?

Please read the Fiori guidelines on tiles, for further details on when to use tiles and which content should be shown.

Do not play videos in their and do not provide any interactive controls like buttons, polls, list. ?

In some use cases the OVP (Overview Page) could be the better choice.

Reading data continuously

Now, for tiles are quickly coming up questions like “How can read the data continuously every X seconds?” Well, you might have seen the dynamic tile. For that you can configure an interval to update the data in the tile. Unfortunately, there is no API to tell the Portal that your custom tile wants to hook into the events like “dashboardTileClick”, “setTilesNoVisibility”, “onHiddenTab”. These events are managed only for DynamicTiles by DashboardLoadingManager.js.

Well, as a workaround you could handle some window/documents events by your own, like “window.blur”, “window.focus” and “document.hidden”.
But, that’s not enough. When you have an update loop running (e.g. via setInterval() to refresh data in your custom tile, it would run the whole time. It also runs when the user has click, on a different tile, endless.

Parameterise Custom Tile via FLP Config – now it’s getting dirty!!

DON’T TRY THIS AT HOME or in PRODUCTION!! ?

Imagine you want to dynamically load data in you custom tile, e.g. a specific type of notifications, within the custom tile. And you don’t want to create another tile app and parameterise the OData/REST call.

Then you could add just another visualisation to the business app configuration, into the visualizations object:

"customTile2-display": {
    "vizType": "project.namespace.customTile2",
    "vizConfig": {
        "sap.app": {
            "title": "{{customTile2.title}}",
            "subTitle": "{{customTile2.subtitle}}"
        },
        "sap.flp": {
            "target": {
                "inboundId": "data-display",
                "parameters": {
                    "param1" : {
                        "value": {
                            "value": "new param value1",
                            "format": "plain"
                        }
                    }
                }
            }
        }
    }

The parameters property is read via a readVisualizations function readVisualizations.getOutbound(). 

During debugging the custom tile, the _oProperties object now has created the target URL with the configured param1 parameter:

targetURL#data-display?param1=new%20param%20value1&sap-ui-app-id-hint=tutorial.app.one.app1

Debug code line: https://github.com/citoki/portal-custom-tile/blob/0d91424d1f373307d49d605d66ed33f69fe7db86/app-custom-tile/webapp/DynamicTile.controller.js#L10

Technically this is the hash/link which should be called after clicking on the custom tile.

You could now parse this string and extract the param1 parameter and use this during the initialisation for the custom tile.

Code – End-To-End Sample

See code sample for an End-To-End sample project: https://github.com/citoki/portal-custom-tile

Assigned Tags

      16 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Bradguver Jesus Villavicencio Rojas
      Bradguver Jesus Villavicencio Rojas

      Many thanks! It is something I was trying days ago, good contribution!

      Author's profile photo CHARANRAJ THARIGONDA
      CHARANRAJ THARIGONDA

      Hi Bradguver Jesus Villavicencio Rojas,

       

      Are you able to create the custom tile? I am getting deployement fail.Please check my code in below comment.

       

      Regards

      Charan

      Author's profile photo Bradguver Jesus Villavicencio Rojas
      Bradguver Jesus Villavicencio Rojas

      Hello CHARANRAJ THARIGONDA, What I could recommend and what in my case solved my problem is that for example in your business-apps.json you put as id not businesscustomapp but something like businesscustomapp.app (keep in mind that you must update the rest and change businesscustomapp by businesscustomapp.app) and after this change create an HTML5 Module where the name of your application would be app and its namespace would be businesscustomapp, after making all these changes make your build and deploy.

      Regards.

      Author's profile photo CHARANRAJ THARIGONDA
      CHARANRAJ THARIGONDA

      Hi Steffen,

      Nice blog. I have tried to replicate the same.However, I got below error. Could you please tell me what could be the reason

       

       

      Author's profile photo Steffen Froehlich
      Steffen Froehlich
      Blog Post Author

      Please check my sample file. Maybe the catalogs definition isn't correct.

       

      https://github.com/citoki/portal-custom-tile/blob/master/flp/portal-site/CommonDataModel.json#L17-L20

      Author's profile photo CHARANRAJ THARIGONDA
      CHARANRAJ THARIGONDA

      Hi Steffen,

      I have tried same way but my deployement is failling.

       

      manifest.json

      {
      	"_version": "1.12.0",
      	"sap.flp": {
      		"type": "tile",
      		"tileSize": "1x1"
      	},
      	"sap.app": {
      		"id": "appui5.appui5",
      		"type": "application",
      		"i18n": "i18n/i18n.properties",
      		"applicationVersion": {
      			"version": "1.0.0"
      		},
      		"title": "{{appTitle}}",
      		"description": "{{appDescription}}",
      		"tags": {
      			"keywords": []
      		},
      		"ach": "CA-UI2-INT-FE",
      		"crossNavigation": {
      			"inbounds": {
      				"products-display": {
      					"signature": {
      						"parameters": {},
      						"additionalParameters": "allowed"
      					},
      					"semanticObject": "products",
      					"action": "display"
      				}
      			}
      		}
      	},
      	"sap.ui": {
      		"technology": "UI5",
      		"icons": {
      			"icon": "",
      			"favIcon": "",
      			"phone": "",
      			"phone@2": "",
      			"tablet": "",
      			"tablet@2": ""
      		},
      		"deviceTypes": {
      			"desktop": true,
      			"tablet": true,
      			"phone": true
      		}
      	},
      	"sap.ui5": {
      		"flexEnabled": false,
      		"componentName": "appui5.appui5",
      		"rootView": {
      			"viewName": "appui5.appui5.view.App",
      			"type": "XML",
      			"async": true,
      			"id": "App"
      		},
      		"dependencies": {
      			"minUI5Version": "1.65.6",
      			"libs": {
      				"sap.ui.core": {},
      				"sap.m": {},
      				"sap.ui.layout": {}
      			}
      		},
      		"contentDensities": {
      			"compact": true,
      			"cozy": true
      		},
      		"models": {
      			"i18n": {
      				"type": "sap.ui.model.resource.ResourceModel",
      				"settings": {
      					"bundleName": "appui5.appui5.i18n.i18n"
      				}
      			}
      		},
      		"resources": {
      			"css": [{
      				"uri": "css/style.css"
      			}]
      		},
      		"handleValidation": false,
      		"routing": {
      			"config": {
      				"routerClass": "sap.m.routing.Router",
      				"viewType": "XML",
      				"async": true,
      				"viewPath": "appui5.appui5.view",
      				"controlAggregation": "pages",
      				"controlId": "app",
      				"clearControlAggregation": false
      			},
      			"routes": [{
      				"name": "RouteApp",
      				"pattern": "RouteApp",
      				"target": ["TargetApp"]
      			}],
      			"targets": {
      				"TargetApp": {
      					"viewType": "XML",
      					"transition": "slide",
      					"clearControlAggregation": false,
      					"viewId": "App",
      					"viewName": "App"
      				}
      			}
      		}
      	}
      }

      business-apps

      {
      	"_version": "3.0.0",
      	"identification": {
      		"id": "businesscustomapp",
      		"entityType": "businessapp",
      		"i18n": "i18n/businessApps.properties"
      	},
      	
      	"payload": {
      
      		"visualizations": {
      
      			"products-displayTocustom": {
      				"vizType": "appui5.appui5",
      				
      				"vizConfig": {
      					"sap.app": {
      						"subTitle": "CustomApp"
      					},
      					"sap.flp": {
      						"target": {
      							"inboundId": "products-display"
      						}
      
      					}
      
      				}
      
      			}
      
      		}
      
      	}
      
      }

       

      CMD file.json

       

      {
      	"_version": "3.0.0",
      	"identification": {
      		"id": "a9c311ca-997a-4d7b-ba57-fbec6c8c0002-1585300229069",
      		"entityType": "bundle"
      	},
      	"payload": {
      		"catalogs": [{
      			"_version": "3.0.0",
      			"identification": {
      				"id": "defaultCatalogId",
      				"title": "{{title}}",
      				"entityType": "catalog",
      				"i18n": "i18n/defaultCatalogId.properties"
      			},
      			"payload": {
      				"viz": [{
      					"id": "businesscustomapp",
      					"vizId": "products-displayTocustom"
      				}]
      			}
      		}],
      		"groups": [{
      			"_version": "3.0.0",
      			"identification": {
      				"id": "defaultGroupId",
      				"title": "{{title}}",
      				"entityType": "group",
      				"i18n": "i18n/defaultGroupId.properties"
      			},
      			"payload": {
      				"viz": [{
      					"id": "businesscustomapp-0-1573035031268",
      					"appId": "businesscustomapp",
      					"vizId": "products-displayTocustom"
      				}]
      			}
      		}],
      		"sites": [{
      			"_version": "3.0.0",
      			"identification": {
      				"id": "504f54af-cda5-47eb-bb3d-05072e32db70-1585300229069",
      				"entityType": "site",
      				"title": "SAP Fiori launchpad site on Cloud Foundry",
      				"description": "SAP Fiori launchpad site on Cloud Foundry, deployed from SAP Web IDE"
      			},
      			"payload": {
      				"config": {
      					"ushellConfig": {
      						"renderers": {
      							"fiori2": {
      								"componentData": {
      									"config": {
      										"applications": {
      											"Shell-home": {}
      										}
      									}
      								}
      							}
      						}
      					}
      				},
      				"groupsOrder": ["defaultGroupId"],
      				"sap.cloud.portal": {
      					"config": {
      						"theme.id": "sap_fiori_3",
      						"theme.active": ["sap_fiori_3", "sap_belize_hcb", "sap_belize_hcw"]
      					}
      				}
      			}
      		}]
      	}
      }

       

      undefined Error : "Failed to search all appHostIds, missing apps: businesscustomapp""

       

       

      Author's profile photo Steffen Froehlich
      Steffen Froehlich
      Blog Post Author

      I think there isn't an UI5 app 'businesscustomapp' deployed in your space/HTML5 repository. The app appui5.appui5 is only the UI5 app for the visualisation of the custom tile. The target app is also needed.

      Author's profile photo Bradguver Jesus Villavicencio Rojas
      Bradguver Jesus Villavicencio Rojas

      Hello CHARANRAJ THARIGONDA, What I could recommend and what in my case solved my problem is that for example in your business-apps.json you put as id not businesscustomapp but something like businesscustomapp.app (keep in mind that you must update the rest and change businesscustomapp by businesscustomapp.app) and after this change create an HTML5 Module where the name of your application would be app and its namespace would be businesscustomapp, after making all these changes make your build and deploy.

      Regards.

      Author's profile photo Lucia Wu
      Lucia Wu

      Hi Steffen,

      It is very useful to me, I have done it like your code, but once I deploy BTP, there are always errors, I don't know what happened, do you have the same situation?

       

      cdm.js:25 2021-08-13 15:10:06.197399 Cannot resolve tile 'com.cimer.gomad.tile#customTileDisplay': no visualization type found for vizTypeId 'com.cimer.gomad.customTile'
       -  sap/ushell/adapters/cdm/LaunchPageAdapter

       

      Best regards,

      Lucia

      Author's profile photo Poornima Ramachandran
      Poornima Ramachandran

      Hi Lucia Wu , were you able to resolve this error?

      Author's profile photo Mohamed Mubashir Alam
      Mohamed Mubashir Alam

      Hello Steffen,

      The blog is very useful, i tried implementing the custom tile to our application, but while deploying its throwing an error

      "Error: Failed to import site, tenantId: 745e90f7-790b-4daf-aad6-c1a2a050d34d, instanceId: 52bb9099-d9a6-4a08-8be2-e4ccb29a0a56, appHostIds: {"appHostIds":["4e4e21d4-0869-4573-8eba-c0b932b0f1f8"],"boundAppHostIds":[],"instanceId":"52bb9099-d9a6-4a08-8be2-e4ccb29a0a56"}, xsAppNameServicesMap: {"html5-apps-repo-dt":"4e4e21d4-0869-4573-8eba-c0b932b0f1f8!b10875|html5-apps-repo-uaa!b1129","com.sap.portal.no.business.service":"irouting-sandbox-bd-internal!b10875"}, blueBoxMetadataStr: undefined Error : "The following IntentHintId defined in CommonDataModel.json do not exists - verify the existence of the following Intents: Set(1) { 'cs.customtile#customtile-manage' }"', error type 'TsInternalServerErrorException'
      Process failed."

      and not allowing to deploy.

      Can you kindly help me in resolving the issue ?

       

      Thanks and Reagrds,

      Alam

       

      Author's profile photo Zameer Ahamad
      Zameer Ahamad

      Hi Alam,

       

      I am also facing the same issue, can you help me how you resolved this issue,

       

      thanks and regards

      Zameer Ahamad

      Author's profile photo Gregor Wolf
      Gregor Wolf

      Hi Steffen,

      thank you for this post. I would suggest you make clear that this is talking about the Launchpad Module that can be used in MTA's and for multi tenant apps. It's not about the Launchpad Service.

      Best Regards
      Gregor

      Author's profile photo Shivaraj Shivaraj
      Shivaraj Shivaraj

      Hi Steffen,

      How to Implement Refresh Interval feature in custom tile, as shown in bellow snapshot?

      Author's profile photo Reena Wadhwa
      Reena Wadhwa

      Hi Shivaraj,

      Can you explain more about how you did it?

      Thanks in Advance!!!

      Author's profile photo Poornima Ramachandran
      Poornima Ramachandran

      Hi, how do i test this in local?