Lightning Dashboard Explorer
This post is both a tutorial on creating a Lightning Component but hopefully a useful component in its own right.
The Reports and Dashboards in Salesforce are great and the temptation is create plenty of reports and also dashboards to show them all off. Shortly after you let a few users have a play and do their own reports. Then one day you try and do a bit of tidying only to find that you cannot delete a report without first removing it from the Dashboard it is on – but Salesforce will not tell you what Dashboard that the report is on !!
So we have two Lightning components. One shows each report and what Dashboard it is on:
and also a list of all the reports on a dashboard.
The code for both is pretty similar.
The code could take a while to process and display given the number of dashboards and reports and the complexity of the number of reports on each dashboard – but the good news is the this processing is all on the client side.
At a high level this is what the components do:
- Get a list of dashboards, reports and then the reports on each dashboard. These are all stored as standard objects and are easy to get of with database calls.
- The critical piece is the table that shows what reports are on what dashboard, we use this to link between.
- Set up client side iterations that sit three deep (hence the delay in presentation). So for instance start with dashboard and loop through the dashboards, then at each dashboard loop through all records that are in the records of reports in the dashboard and then
- Present to the users via a simple lightning:accordion with a data table inside
On face value step 3 looks pretty awful and indeed there is definitely a more direct way to do this around an Apex call that starts with say a dashboard id and returns the reports that are on the dashboard. Give that this is not likely to be used daily the simplicity of the presentation code wins over the complexity of a call passing a dashboard id to return a result set. That being said it is a bit painful to resort to coding that is ‘brute force’ rather than ‘elegance’.
The apex controller Dashboards.apxc is pretty simple with three separate functions:
public class Dashboards {
@AuraEnabled
public static List<Report> returnReports() {
List<Report> reportList = [SELECT Id,FolderName,Name FROM Report];
return reportList;
}
@AuraEnabled
public static List<Dashboard> returnDashboards() {
List<Dashboard> dashboardList = [SELECT Id, FolderName,Title FROM Dashboard];
return dashboardList;
}
@AuraEnabled
public static List<DashboardComponent> returnDashboardsComponents() {
List<DashboardComponent> dashboardComponentList = [SELECT Id, CustomReportId,DashboardId,Name FROM DashboardComponent];
return dashboardComponentList;
}
}
This queries all three tables in the Saleforce instance – Report holds the reports, Dashboard holds the dashboard and the then DashboardComponent links the two together.
Create a new Lightning Component called DashboardExplorer and modify it to start with:
<aura:component implements="flexipage:availableForAllPageTypes" access="global" controller="Dashboards">
<aura:attribute name="reports" type="Report[]"/>
<aura:attribute name="dashboards" type="Dashboard[]"/>
<aura:attribute name="dashboardComponents" type="DashboardComponent[]"/>
<aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
</aura:component>
This get the component setup with the implements=”flexipage:availableForAllPageTypes” so that this can sit on it own Lightning page, controller=”Dashboard” so that we can call the apex controller functions.
The three attributes are set as lists to hold the reports, dashboards and components.
Finally a handler is set to call the doInit on the lightning controller. The code for this is:
({
doInit : function(component, event, helper) {
var action = component.get("c.returnDashboards");
// Create a callback that is executed after
// the server-side action returns
action.setCallback(this, function(response) {
var state = response.getState();
if (state === "SUCCESS") {
// Alert the user with the value returned
// from the server
//var reports = component.get("v.reports");
component.set("v.dashboards",response.getReturnValue());
// You would typically fire a event here to trigger
// client-side notification that the server-side
// action is complete
}
else if (state === "INCOMPLETE") {
// do something
}
else if (state === "ERROR") {
var errors = response.getError();
if (errors) {
if (errors[0] && errors[0].message) {
console.log("Error message: " +
errors[0].message);
}
} else {
console.log("Unknown error");
}
}
});
$A.enqueueAction(action);
var action1 = component.get("c.returnReports");
// Create a callback that is executed after
// the server-side action returns
action1.setCallback(this, function(response) {
var state = response.getState();
if (state === "SUCCESS") {
// Alert the user with the value returned
// from the server
//var reports = component.get("v.reports");
component.set("v.reports",response.getReturnValue());
// You would typically fire a event here to trigger
// client-side notification that the server-side
// action is complete
}
else if (state === "INCOMPLETE") {
// do something
}
else if (state === "ERROR") {
var errors = response.getError();
if (errors) {
if (errors[0] && errors[0].message) {
console.log("Error message: " +
errors[0].message);
}
} else {
console.log("Unknown error");
}
}
});
$A.enqueueAction(action1);
var action2 = component.get("c.returnDashboardsComponents");
// Create a callback that is executed after
// the server-side action returns
action2.setCallback(this, function(response) {
var state = response.getState();
if (state === "SUCCESS") {
// Alert the user with the value returned
// from the server
//var reports = component.get("v.reports");
component.set("v.dashboardComponents",response.getReturnValue());
// You would typically fire a event here to trigger
// client-side notification that the server-side
// action is complete
}
else if (state === "INCOMPLETE") {
// do something
}
else if (state === "ERROR") {
var errors = response.getError();
if (errors) {
if (errors[0] && errors[0].message) {
console.log("Error message: " +
errors[0].message);
}
} else {
console.log("Unknown error");
}
}
});
$A.enqueueAction(action2);
},
})
This looks complicated but the reality is that is three separate asynchronous calls to fetch the three objects that we need.
At this point we have all the data we need, we can move to the presentation. Again from a conceptual point of view this is a difficult pill to swallow, we have the data from the server and now move to the client side to borrow local resources to present the data.
Let’s build this up layer by layer. Without any code a lightning component has no background so a lightning:card provides a canvas on which we can build (passing a title and icon for consistency). The lightning:according comes with some ‘free’ features (always try and use these over the slds see this documentation before the slds documentation).
The aura:iteration will loop nicely through the dashboards and we can use a lightning:accordionSection to show the dashboard and also the folder name of the dashboard. We also start the ‘internal’ lightning:card for the reports in the Dashboard.
<lightning:card title="Dashboard Explorer" iconName="standard:dashboard">
<lightning:accordion >
<aura:iteration items="{!v.dashboards}" var="dashboard">
<lightning:accordionSection name="{!dashboard.Id}" label="{!dashboard.Title + ' (folder ' + dashboard.FolderName + ')'}">
<lightning:card title="Reports in this Dashboard" iconName="standard:report">
<aura:set attribute="actions">
<lightning:button onclick="{! c.openDashboard }" name="{! dashboard.Id}" label="Open Dashboard"/>
</aura:set>
</lightning:card>
</lightning:accordionSection>
</aura:iteration>
</lightning:accordion>
</lightning:card>
This code also has an onClick functions called openDashboard that will open up the dashboard. The name attribute is set to be the Dashboard.Id so that when the onclick is called we can query to query the id and open that as an item.
In the controller this function is
openDashboard : function (component, event, helper) {
var id = event.getSource().get("v.name");
console.log(id);
var navEvt = $A.get("e.force:navigateToSObject");
navEvt.setParams({
"recordId": id,
});
navEvt.fire();
},
If this was run by itself you would get an accoridon of all the Dashboards and an Open button to open the dashboard. We now need a table for each dashboard with the reports on the Dashboard.
This is the code inside the inner lightning:card
<table class="slds-table slds-table_bordered slds-table_cell-buffer">
<thead>
<tr class="slds-text-title_caps">
<th scope="col">
<div class="slds-truncate" title="Report Title">Report Title</div>
</th>
<th scope="col">
<div class="slds-truncate" title="Report Folder">Report Folder</div>
</th>
<th scope="col">
<div class="slds-truncate" title="Open Report">Open Report</div>
</th>
</tr>
</thead>
<tbody>
<!-- pass through each dashboard component looking for dashboardid, this provides a report Id-->
<aura:iteration items="{!v.dashboardComponents}" var="dashboardComponent">
<aura:if isTrue="{!dashboardComponent.DashboardId == dashboard.Id}">
<!-- pass through each report looking for dashboardid, this provides a report Id-->
<aura:iteration items="{!v.reports}" var="report">
<aura:if isTrue="{!dashboardComponent.CustomReportId == report.Id}">
<tr>
<th scope="row" data-label="Report Title">
<div class="slds-truncate" title="{!report.Name}">{!report.Name}</div>
</th>
<td data-label="Report Folder">
<div class="slds-truncate" title="{!report.FolderName}">{!report.FolderName}</div>
</td>
<td data-label="Open Report">
<lightning:button onclick="{! c.openReport }" name="{! report.Id}" label="Open"/> </td>
</tr>
</aura:if>
</aura:iteration>
</aura:if>
</aura:iteration>
</tbody>
</table>
The lighning:datatable is a potential different solution but as we dealing with data that we already have an d we need to choose the exact rows we are displaying we will need to create each row by hand. So we are using the <table class=”slds-table slds-table_bordered slds-table_cell-buffer”> see the slds documentation. The first section sets up the headings for the table.
Now we use aura:iteration to go through all the records in the DasboardComponents – looking for a match to the dashboardComponent.DashboardId == dashboard.Id to find a report that is on the dashboard. If we hit then we have a reportId (held in dashboardComponent.CustomReportid) but no name or folder so we have another aura:iteration looking for the report so there is a third aura:iteration. Finally we get to present the details of the report and also a button to open the report openReport again passing the report id in the name attribute. In the controller we have:
openReport : function (component, event, helper) {
var id = event.getSource().get("v.name");
console.log(id);
var navEvt = $A.get("e.force:navigateToSObject");
navEvt.setParams({
"recordId": id,
});
navEvt.fire();
},
this code is the same as openDashboard but is kept as a separate function for a bit of clarity.
So the final code is:
<aura:component implements="flexipage:availableForAllPageTypes" access="global" controller="Dashboards">
<aura:attribute name="reports" type="Report[]"/>
<aura:attribute name="dashboards" type="Dashboard[]"/>
<aura:attribute name="dashboardComponents" type="DashboardComponent[]"/>
<aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
<lightning:card title="Dashboard Explorer" iconName="standard:dashboard">
<lightning:accordion >
<aura:iteration items="{!v.dashboards}" var="dashboard">
<lightning:accordionSection name="{!dashboard.Id}" label="{!dashboard.Title + ' (folder ' + dashboard.FolderName + ')'}">
<lightning:card title="Reports in this Dashboard" iconName="standard:report">
<aura:set attribute="actions">
<lightning:button onclick="{! c.openDashboard }" name="{! dashboard.Id}" label="Open Dashboard"/>
</aura:set>
<table class="slds-table slds-table_bordered slds-table_cell-buffer">
<thead>
<tr class="slds-text-title_caps">
<th scope="col">
<div class="slds-truncate" title="Report Title">Report Title</div>
</th>
<th scope="col">
<div class="slds-truncate" title="Report Folder">Report Folder</div>
</th>
<th scope="col">
<div class="slds-truncate" title="Open Report">Open Report</div>
</th>
</tr>
</thead>
<tbody>
<!-- pass through each dashboard component looking for dashboardid, this provides a report Id-->
<aura:iteration items="{!v.dashboardComponents}" var="dashboardComponent">
<aura:if isTrue="{!dashboardComponent.DashboardId == dashboard.Id}">
<!-- pass through each report looking for dashboardid, this provides a report Id-->
<aura:iteration items="{!v.reports}" var="report">
<aura:if isTrue="{!dashboardComponent.CustomReportId == report.Id}">
<tr>
<th scope="row" data-label="Report Title">
<div class="slds-truncate" title="{!report.Name}">{!report.Name}</div>
</th>
<td data-label="Report Folder">
<div class="slds-truncate" title="{!report.FolderName}">{!report.FolderName}</div>
</td>
<td data-label="Open Report">
<lightning:button onclick="{! c.openReport }" name="{! report.Id}" label="Open"/> </td>
</tr>
</aura:if>
</aura:iteration>
</aura:if>
</aura:iteration>
</tbody>
</table>
</lightning:card>
</lightning:accordionSection>
</aura:iteration>
</lightning:accordion>
</lightning:card>
</aura:component>
and the controller code is :
({
doInit : function(component, event, helper) {
var action = component.get("c.returnDashboards");
// Create a callback that is executed after
// the server-side action returns
action.setCallback(this, function(response) {
var state = response.getState();
if (state === "SUCCESS") {
// Alert the user with the value returned
// from the server
//var reports = component.get("v.reports");
component.set("v.dashboards",response.getReturnValue());
// You would typically fire a event here to trigger
// client-side notification that the server-side
// action is complete
}
else if (state === "INCOMPLETE") {
// do something
}
else if (state === "ERROR") {
var errors = response.getError();
if (errors) {
if (errors[0] && errors[0].message) {
console.log("Error message: " +
errors[0].message);
}
} else {
console.log("Unknown error");
}
}
});
$A.enqueueAction(action);
var action1 = component.get("c.returnReports");
// Create a callback that is executed after
// the server-side action returns
action1.setCallback(this, function(response) {
var state = response.getState();
if (state === "SUCCESS") {
// Alert the user with the value returned
// from the server
//var reports = component.get("v.reports");
component.set("v.reports",response.getReturnValue());
// You would typically fire a event here to trigger
// client-side notification that the server-side
// action is complete
}
else if (state === "INCOMPLETE") {
// do something
}
else if (state === "ERROR") {
var errors = response.getError();
if (errors) {
if (errors[0] && errors[0].message) {
console.log("Error message: " +
errors[0].message);
}
} else {
console.log("Unknown error");
}
}
});
$A.enqueueAction(action1);
var action2 = component.get("c.returnDashboardsComponents");
// Create a callback that is executed after
// the server-side action returns
action2.setCallback(this, function(response) {
var state = response.getState();
if (state === "SUCCESS") {
// Alert the user with the value returned
// from the server
//var reports = component.get("v.reports");
component.set("v.dashboardComponents",response.getReturnValue());
}
else if (state === "INCOMPLETE") {
// do something
}
else if (state === "ERROR") {
var errors = response.getError();
if (errors) {
if (errors[0] && errors[0].message) {
console.log("Error message: " +
errors[0].message);
}
} else {
console.log("Unknown error");
}
}
});
$A.enqueueAction(action2);
},
openDashboard : function (component, event, helper) {
var id = event.getSource().get("v.name");
console.log(id);
var navEvt = $A.get("e.force:navigateToSObject");
navEvt.setParams({
"recordId": id,
});
navEvt.fire();
},
openReport : function (component, event, helper) {
var id = event.getSource().get("v.name");
console.log(id);
var navEvt = $A.get("e.force:navigateToSObject");
navEvt.setParams({
"recordId": id,
});
navEvt.fire();
},
})
this is code for the Dashboard explorer – this shows by dashboard all the reports on each dashboard:
The code for the ReportExplorer is identical on the controller side but on the client side is:
<aura:component implements="flexipage:availableForAllPageTypes" access="global" controller="Dashboards">
<aura:attribute name="reports" type="Report[]"/>
<aura:attribute name="dashboards" type="Dashboard[]"/>
<aura:attribute name="dashboardComponents" type="DashboardComponent[]"/>
<aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
<lightning:card title="Report Explorer" iconName="standard:report">
<lightning:accordion >
<aura:iteration items="{!v.reports}" var="report">
<lightning:accordionSection name="{!report.Id}" label="{!report.Name + ' (folder ' + report.FolderName + ')'}">
<lightning:card title="Used in Dashboard" iconName="standard:dashboard">
<aura:set attribute="actions">
<lightning:button onclick="{! c.openReport }" name="{! report.Id}" label="Open Report"/>
</aura:set>
<table class="slds-table slds-table_bordered slds-table_cell-buffer">
<thead>
<tr class="slds-text-title_caps">
<th scope="col">
<div class="slds-truncate" title="Dashboard Title">Dashboard Title</div>
</th>
<th scope="col">
<div class="slds-truncate" title="Dashboard Folder">Dashboard Folder</div>
</th>
<th scope="col">
<div class="slds-truncate" title="Open Dashboard">Open Dashboard</div>
</th>
</tr>
</thead>
<tbody>
<!-- pass through each dashboard component looking for reportid, this provides a dashboard Id-->
<aura:iteration items="{!v.dashboardComponents}" var="dashboardComponent">
<aura:if isTrue="{!dashboardComponent.CustomReportId == report.Id}">
<!-- pass through each dashboard looking for dashboardid, this provides a report Id-->
<aura:iteration items="{!v.dashboards}" var="dashboard">
<aura:if isTrue="{!dashboardComponent.DashboardId == dashboard.Id}">
<tr>
<th scope="row" data-label="Dashboard Title">
<div class="slds-truncate" title="{!dashboard.Title}">{!dashboard.Title}</div>
</th>
<td data-label="Report Folder">
<div class="slds-truncate" title="{!dashboard.FolderName}">{!dashboard.FolderName}</div>
</td>
<td data-label="Open Dashboard">
<lightning:button onclick="{! c.openDashboard }" name="{! dashboard.Id}" label="Open"/> </td>
</tr>
</aura:if>
</aura:iteration>
</aura:if>
</aura:iteration>
</tbody>
</table>
</lightning:card>
</lightning:accordionSection>
</aura:iteration>
</lightning:accordion>
</lightning:card>
</aura:component>
this does almost the same approach but loops the aura:iterations in a slightly different order, starting with the report and seeing what dashboards this belongs to.