Using Network Graph in a List Report Fiori Template
Last year we were contacted by a couple of people trying to integrate a Network Graph control into an object page. As this might be a common scenario for Fiori apps, we decided to put together this step-by-step guide that will lead you through the process.
Facet Breakout
The most common way to include a custom control into an object page is to implement a Facet Breakout. With a Facet Breakout, you can define a custom view for a portion of your object page. The problem is that a facet was designed to hold a smaller amount of content and is placed into an infinite scrolling container with all other facets. Network Graph, on the other hand, was designed as a content-rich component. It is expected to visualize more data and to take up most of the space on the page. It also supports zooming and has its own scroll bars. If you place it in a Facet Breakout, you will most probably end up with double scroll bars and poor user experience.
Another problem with a Facet Breakout is that the header section on an object page is quite large and it eats up the precious space that could be used to visualize the graph itself. With larger network graphs, this could be quite annoying.
That’s why we would recommend that you include the Network Graph in a Page-Level Breakout instead. If you still would like to place a Network Graph inside a Facet Breakout, we would recommend that you do that only for a small number of nodes. You should apply a fixed height and set enableWheelZoom
to false
. This will prevent the network graph from interfering with the facet container scrolling.
Page-Level Breakout
Our demo application will visualize a list of documents. Each document represents one step in a complex process our company has implemented to process orders. To allow the user to figure out quickly what the document represents, we want to provide them with an option to click a document in a list report and see it in a network graph representing the entire process.
Before we create a Page-Level Breakout, we need an OData service and a List Report application. If you already have both, you can skip the following two section and continue with Creating Your Own UI Component.
OData Service
We are going to build a simple OData service using a CDS view and a Gateway. In the CDS view, we will use a simple table named general_table
:
ID | Type | Title | Value | Status | CreatedFld | Ref1 | Ref2 |
---|---|---|---|---|---|---|---|
1 | NODE | Customer Order | Success | 20180601 | |||
2 | NODE | Proof of Shipment | Standard | 20180604 | |||
3 | NODE | Generated Invoice | Error | 20180605 | |||
4 | LINE | 1 | 2 | ||||
5 | LINE | 1 | 3 | ||||
6 | ATTR | Document Type | Order | 1 | |||
7 | ATTR | Processor | 356859 | 1 | |||
8 | ATTR | Document Type | Shipment | 2 | |||
9 | ATTR | Processor | 258456 | 2 | |||
10 | ATTR | Document Type | Invoice | 3 | |||
11 | ATTR | Processor | 123586 | 3 |
We will need 4 entity sets for our app. The first entity set will be used as a model for a list of documents.
define view DOCUMENTS as select general_table as main
left outer to one join general_table as attr on attr.type = 'ATTR' and attr.title = 'Document Type' and main.id = attr.ref1
association [0..1] to NODES as _node on $projection.Id = _node.Id
{
@UI: {
lineItem: {position: 10},
selectionField: {position: 10}
}
key main.id as Id,
@UI: {
lineItem: {position: 20}
}
main.title as Title,
@UI: {
lineItem: {position: 30},
selectionField: {position: 20}
}
attr.value as DocumentType,
@UI: {
lineItem: {position: 40},
selectionField: {position: 30}
}
main.createdfld as Createdm,
_node
} where main.type = 'NODE'
The other three entity sets will provide the models for the data needed for network graph. In our app, we will only use nodes, lines, and attributes. If we want to join nodes into groups and include node details, we would need more entity sets.
define view NODES as select from general_table
association [0..*] to ATTRIBUTES as _attribute on $projection.Id = _attribute.ParentNode
{
key id as Id,
title as Title,
status as Status,
_attribute
} where type = 'NODE'
define view ATTRIBUTES as select from general_table {
key id as Id,
ref1 as ParentNode,
title as LabelC,
value as Value
} where type = 'ATTR'
define view LINES as select from general_table {
key id as Id,
ref1 as IdFrom,
ref2 as IdTo
} where type = 'LINE'
Creating a Simple List Report Application
To create a simple List Report application, we will use SAP Web IDE (https://www.sap.com/developer/topics/sap-webide.html).
- In the SAP Web IDE, choose File > New > Project from Template and select List Report Application.
- Provide a project name and title and point the wizard to your OData service.
- Pick the pre-generated annotation, even though we don’t need annotations in this example.
- In the Template Customization step, select the DOCUMENTS entity set as OData Collection and click Finish.
You can try to run the application in Sandbox mode. When the application loads, you should see a list report application with a table that contains 3 documents. When you click on an item in the table, you should see an empty object page.
Creating Your Own UI Component
Now we need to create a UI component that we will use for the Canvas breakout. To learn more about UI components, see Components.
- Create a new folder named
component
inside the webapp folder. In the newly created component folder, create a newComponent.js
file:Component.js
:sap.ui.define(["sap/ui/core/UIComponent", "sap/suite/ui/generic/template/extensionAPI/ReuseComponentSupport"], function(UIComponent, ReuseComponentSupport) { "use strict"; return UIComponent.extend("BlogTest.component.Component", { metadata: { manifest: "json", library: "BlogTest.component", properties: { /** Properties for reuse component */ uiMode: { type: "string", group: "standard" }, semanticObject: { type: "string", group: "standard" }, stIsAreaVisible: { type: "boolean", group: "standard" } } }, init: function() { ReuseComponentSupport.mixInto(this, "component"); (UIComponent.prototype.init || jQuery.noop).apply(this, arguments); }, stStart: function(oModel, oBindingContext, oExtensionAPI) { var oComponentModel = this.getComponentModel(); this.getRootControl().setModel(oModel); oComponentModel.setProperty("/navigationController", oExtensionAPI.getNavigationController()); oComponentModel.setProperty("/selectedNode", oExtensionAPI.getNavigationController().getCurrentKeys()[1]); this.extensionAPI = oExtensionAPI; }, stRefresh: function(oModel, oBindingContext) { var oComponentModel = this.getComponentModel(); var oNavigationController = oComponentModel.getProperty("/navigationController"); oComponentModel.setProperty("/selectedNode", oNavigationController.getCurrentKeys()[1]); this.getRootControl().getController().updateSelection(); } }); });
- The
stStart
function is called each time the List Report initializes the object page for the first time. stRefresh
is called whenever user navigates back to object page from the List Report.Please note that the page stays initialized even when the user clicks the Back button and returns back to the List Report. When the user navigates to the object page again, theinit
method is not called on the controller anymore andstRefresh
is the most convenient method to use if you want to get notified about this event.
Each time the user navigates to the object page, we will store the ID of the document the user used for the navigation to be able to select corresponding node. The ID of the element the user clicks is stored in the navigation controller and can be fetched using the
getCurrentKeys
method. We will store this value in the component model, which can be accessed in the controller. We also need to call theupdateSelection
method in the controller (we will define this method in one of the next steps). - The
- Now we need to create a view and its controller. Just like the view and the controller of the main app, we will put these new view and controller into the corresponding folders,
view
andcontroller
.Default.view.xml
:<mvc:View xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m" xmlns:ng="sap.suite.ui.commons.networkgraph" controllerName="BlogTest.component.controller.Default" > <App> <pages> <Page id="group" title="{i18n>groupTitle}"> <content> <ng:Graph id="networkGraph" nodes="{/NODES}" lines="{/LINES}"> <ng:nodes> <ng:Node key="{Id}" title="{Title}" shape="Box" attributes="{path:'to_attribute', templateShareable:false}" status="{Status}"> <ng:attributes> <ng:ElementAttribute label="{LabelC}" value="{Value}" /> </ng:attributes> </ng:Node> </ng:nodes> <ng:lines> <ng:Line from="{IdFrom}" to="{IdTo}" /> </ng:lines> </ng:Graph> </content> </Page> </pages> </App> </mvc:View>
Default.controller.js
:sap.ui.define([ "sap/suite/ui/generic/template/extensionAPI/extensionAPI" ], function(extensionAPI) { "use strict"; return sap.ui.controller("BlogTest.component.controller.Default", { onInit: function() { var oGraph = this.getView().byId("networkGraph"); oGraph.attachBeforeLayouting(function () { this.bGraphReady = false; }.bind(this)); this.getView().byId("networkGraph").attachGraphReady(function () { this.bGraphReady = true; this.selectNode(); }.bind(this)); }, selectNode: function () { var oComponent = this.getOwnerComponent(); var oComponentModel = oComponent.getComponentModel(); var sSelectedNode = oComponentModel.getProperty("/selectedNode"); var oGraph = this.getView().byId("networkGraph"); var oNode = oGraph.getNodeByKey(sSelectedNode); if (oNode) { oGraph.getNodes().forEach(function (oN) { oN.setSelected(false); }); oNode.setSelected(true); oGraph.scrollToElement(oNode); } }, updateSelection: function () { if (this.bGraphReady) { this.selectNode(); } } }); });
A few tricks here. We want to make sure that a node user selected on the List Report is selected and visible when the graph loads. This needs to be done when the graph is rendered. Remember that Network Graph first renders an empty placeholder and starts the layouting worker task which can take few seconds. When the layouting is done, the graph is rendered again and
graphReady
event is fired. If you need to do anything with the graph after it’s rendered, this is the event to use. We will also use newly introducedscrollToElement
function, which ensures, that given element (node, line or group) will be in the visible viewBox. This is important for big graphs as the node can be outside of visible area. - We also need to create a simple i18n.properties file that contains translatable text strings and a
manifest.json
file:i18n.properties
:#XTIT: Application name appTitle= #YDES: Application description appDescription= #XTIT: Application name groupTitle=An Additional Title
manifest.json
:{ "_version": "1.7.0", "sap.app": { "_version": "1.1.0", "id": "BlogTest.component", "type": "component", "i18n": "i18n/i18n.properties", "title": "{{appTitle}}", "description": "{{appDescription}}", "ach": "xxx", "offline": false, "resources": "resources.json", "embeddedBy": "${project.artifactId}", "applicationVersion": { "version": "${project.version}" } }, "sap.ui": { "_version": "1.1.0", "technology": "UI5", "deviceTypes": { "desktop": true, "tablet": true, "phone": true } }, "sap.ui5": { "_version": "1.1.0", "rootView": "BlogTest.component.view.Default", "dependencies": { "minUI5Version": "${sap.ui5.dist.version}", "libs": {} }, "models": { "i18n": { "type": "sap.ui.model.resource.ResourceModel", "uri": "i18n/i18n.properties" } }, "contentDensities": { "compact": true, "cozy": true } } }
- Now it’s time to point the List Report template to the component. We will use
sap.suite.ui.generic.template.Canvas
for this.In your manifest.json file find thesap.ui.generic.app
section. The configuration should have apages
property that was configured to be used with a List Report. This configuration was generated by the WebIDE wizard. You need to change the ListReportpages
configuration. Since there is a couple of pages properties involved, I’ll include the entiresap.ui.generic.app
configuration:sap.ui.generic.app
"sap.ui.generic.app": { "_version": "1.3.0", "settings": {}, "pages": { "ListReport|DOCUMENTS": { "entitySet": " DOCUMENTS", "component": { "name": "sap.suite.ui.generic.template.ListReport", "list": true, "settings": { "smartVariantManagement": true } }, "pages": { "Canvas|NODES": { "routingSpec": { "noOData": true, "routeName": "NODES", "typeImageUrl": "" }, "navigationProperty": "to_node", "entitySet": "NODES", "component": { "name": "sap.suite.ui.generic.template.Canvas", "settings": { "requiredControls": { "footerBar": false, "paginatorButtons": false } } }, "implementingComponent": { "componentName": "BlogTest.component" } } } } } }
- However, this is not enough. If you try running the app now, the navigation will open the page with Network Graph, but the selection of corresponding node will not work.We need to create a controller extension and define the navigation manually. In order to do that, you must create an
ext
folder in thewebapp
folder, and create acontroller
folder inside theext
folder. In thecontroller
folder, create aListReportExt.controller.js
file:ListReportExt.controller.js
:sap.ui.define([ "sap/suite/ui/generic/template/extensionAPI/extensionAPI" ], function(extensionAPI) { "use strict"; return sap.ui.controller("BlogTest.ext.controller.ListReportExt", { onInit: function () { }, onListNavigationExtension: function(oEvent) { var oExtensionAPI = this.extensionAPI; var oNavigationController = oExtensionAPI.getNavigationController(); var oItem = oEvent.getSource(); var oBindingContext = oItem.getBindingContext(); var oObject = oBindingContext.getObject(); oNavigationController.navigateInternal(oObject.Id, { routeName: "NODES" }); return true; } }); });
If you already have a controller extension for the List Report, you can just define the
onListNavigationExtension
method. You must also define the controller extension inmanifest.json
insap.ui5
configuration:manifest.json
:"extends": { "extensions": { "sap.ui.controllerExtensions": { "sap.suite.ui.generic.template.ListReport.view.ListReport": { "controllerName": "BlogTest.ext.controller.ListReportExt", "sap.ui.generic.app": { "DOCUMENTS": { "EntitySet": "DOCUMENTS" } } } } } }
- At the end, your object page should look similar to this:
Thank you, very useful!