In today’s data-driven world, businesses rely on analytics tools to make informed decisions. Salesforce, a leading CRM platform, offers powerful reporting capabilities, but integrating external analytics tools like Power BI can enhance its functionality. This blog post explores how Power BI was successfully integrated into Salesforce to streamline reporting and provide seamless user experiences.
Custom Metadata Types were created to store Power BI App Credentials and Report Details. These metadata records included:
HTML File -
<template>
<lightning-card title={reportName} icon-name="utility:table">
<div class="slds-m-around_medium">
<template if:true={report}>
<template if:true={report.error}>
<div class="slds-text-color_error">
<lightning-icon icon-name="utility:error" alternative-text="Error" class="slds-m-right_x-small"></lightning-icon>
<span>{report.error}</span>
</div>
</template>
<template if:true={report.embedUrl}>
<div data-id="embed-container" class="report-container slds-grid slds-wrap slds-align_absolute-center"></div>
</template>
</template>
<template if:false={report}>
<lightning-spinner alternative-text="Loading Power BI report" size="medium"></lightning-spinner>
</template>
</div>
</lightning-card>
</template>
JS File -
import { LightningElement, wire, track, api } from 'lwc';
import getEmbeddingDataForReport from '@salesforce/apex/PowerBiEmbedManager.getEmbeddingDataForReport';
import powerbijs from '@salesforce/resourceUrl/powerbijs';
import { loadScript } from 'lightning/platformResourceLoader';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import { CurrentPageReference } from 'lightning/navigation';
import SalesforceGlobalHeaderHide from '@salesforce/resourceUrl/SalesforceGlobalHeaderHide';
import { loadStyle } from 'lightning/platformResourceLoader';
import Complex_Dealer_Code_FIELD from '@salesforce/schema/Account.Complex_Dealer_Code__c'
import SAP_Code_FIELD from '@salesforce/schema/Account.SAP_Code__c'
import Brand_Name_FIELD from '@salesforce/schema/Account.Brand_Name__c'
const fields = [Complex_Dealer_Code_FIELD, SAP_Code_FIELD, Brand_Name_FIELD];
export default class PowerBiReportEmbed extends LightningElement {
@api selectedFlexiPage = '';
@api recordId;
@track report;
@track error;
@track storeComplexDealercode = '';
@track storeComplexDealerCode;
@track storeSapCode;
@track storeBrandName;
@track reportName;
@track getcmyStatevalue = '';
@wire(CurrentPageReference)
getPageReference(pageRef) {
if (pageRef) {
this.getcmyStatevalue = pageRef.state.c__mystate || '';
console.log('this.param-->', JSON.stringify(this.getcmyStatevalue));
if (this.getcmyStatevalue) {
loadStyle(this, SalesforceGlobalHeaderHide)
.then(() => {
console.log('Custom CSS loaded successfully');
})
.catch(error => {
console.error('Error loading custom CSS:', error);
});
}
}
}
@wire(getRecord, { recordId: '$recordId', fields: fields })
wiredAccount({ error, data }) {
if (data) {
this.storeComplexDealerCode = getFieldValue(data, Complex_Dealer_Code_FIELD);
this.storeSapCode = getFieldValue(data, SAP_Code_FIELD);
this.storeBrandName = this.normalizeBrandName(getFieldValue(data, Brand_Name_FIELD));
console.log('Fetched Account Data:', { recordId: this.recordId, brandName: this.storeBrandName });
} else if (error) {
console.error('Error fetching account data:', error);
this.showToast('Error', 'Failed to load Account details.', 'error');
}
}
normalizeBrandName(brandName) {
switch (brandName) {
case 'NHAG': return 'NHA';
case 'CSAG': return 'CIH';
case 'CSCE': return 'CSE';
default: return brandName;
}
}
@wire(getEmbeddingDataForReport, { selectedFlexiPage: '$selectedFlexiPage', mystateValue: '$getcmyStatevalue' })
wiredEmbeddingData({ error, data }) {
if (data) {
this.report = data;
this.reportName = data.name;
console.debug('Power BI Report Data:', this.report);
} else if (error) {
this.report = undefined;
this.error = error;
console.error('Error fetching Power BI report:', error);
this.showToast('Error', 'Failed to load Power BI embedding data.', 'error');
}
}
renderedCallback() {
Promise.all([loadScript(this, powerbijs)]).then(() => {
if (this.report && this.report.embedUrl && this.report.embedToken) {
console.log('Initializing Power BI Report Embed...');
const reportContainer = this.template.querySelector('[data-id="embed-container"]');
const reportId = this.report.reportId;
const embedUrl = this.report.embedUrl;
const embedToken = this.report.embedToken;
const embedUrlWithFilter = this.generateFilteredEmbedUrl(embedUrl);
const config = {
type: 'report',
id: reportId,
embedUrl: embedUrlWithFilter,
accessToken: embedToken,
tokenType: 1,
settings: {
panes: {
bookmarks: {
visible: false
},
fields: {
expanded: false
},
filters: {
expanded: true,
visible: true
},
pageNavigation: {
visible: true
},
selection: {
visible: false
},
syncSlicers: {
visible: false
},
visualizations: {
expanded: false
}
},
zoomLevel: 1,
filterPaneEnabled: true,
navContentPaneEnabled: true
}
};
try {
powerbi.embed(reportContainer, config);
console.log('Report successfully embedded');
this.showToast('Success', 'PowerBI report loaded successfully.', 'success');
} catch (error) {
console.error('Error embedding Power BI report:', error);
this.showToast('Error', 'Failed to load PowerBI report. Please contact System Adminstrator.', 'error');
}
} else {
console.log('Report data is incomplete or missing' + JSON.stringify(error));
this.showToast('Error', 'Failed to load PowerBI report. Please contact System Adminstrator.', 'error');
}
});
}
generateFilteredEmbedUrl(embedUrl) {
let filter = '';
if (this.selectedFlexiPage === 'Retail MS C1') {
filter = `filter=ft_market_share_by_dealer/CD_SAP_CODE%20eq%20%27${encodeURIComponent(this.storeSapCode)}%27%20and%20ft_market_share_by_dealer/CD_BRAND_CODE%20eq%20%27${encodeURIComponent(this.storeBrandName)}%27`;
} else if (this.selectedFlexiPage === 'Retail MS C3') {
filter = `filter=ft_market_share_by_dealer/CD_SAP_CODE%20eq%20%27${encodeURIComponent(this.storeComplexDealerCode)}%27` +
`%20and%20ft_market_share_by_dealer/CD_BRAND_CODE%20eq%20%27${encodeURIComponent(this.storeBrandName)}%27` +
`%20and%20ft_market_share_by_dealer/C1_x002F_C2_x002F_C3%20eq%20%27${encodeURIComponent('C3')}%27`;
}
return filter ? `${embedUrl}${embedUrl.includes('?') ? '&' : '?'}${filter}` : embedUrl;
}
showToast(title, message, variant) {
this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
}
}
CSS File -
.report-container {
height: 750px;
border: 1px solid #d8dde6;
padding: 10px;
background-color: #f3f4f6;
border-radius: 0.5rem;
}
XML File -
<?xml version="1.0"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>62.0</apiVersion>
<description>PowerBI integration</description>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__UrlAddressable</target>
<target>lightning__Tab</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordPage,lightning__AppPage">
<property name="selectedFlexiPage" label="Please Select" type="String" datasource="Retail MS C1, Retail MS C3"/>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
Apex Class -
public class PowerBiEmbedManager {
@AuraEnabled(cacheable=true)
public static List <Power_BI_App_Report_Details__mdt> getPowerBiAppReportDetails() {
return [SELECT DeveloperName,Enable_Sharepoint_URL__c,Sharepoint_URL__c FROM Power_BI_App_Report_Details__mdt];
}
@AuraEnabled(cacheable=true)
public static PowerBiReportData getEmbeddingDataForReport(String selectedFlexiPage, String mystateValue) {
System.debug('Entering getEmbeddingDataForReport with selectedFlexiPage: ' + selectedFlexiPage + ', mystateValue: ' + mystateValue);
String workspaceId = '';
String reportId = '';
Boolean isPurchaseProspect = false;
PowerBiReportData reportData = new PowerBiReportData();
try {
System.debug('Fetching metadata instances...');
Map<String, Power_BI_App_Report_Details__mdt> reportDetailsMap = new Map<String, Power_BI_App_Report_Details__mdt>{
'ReXXXXXXX1' => Power_BI_App_Report_Details__mdt.getInstance('Retail_MS_C1'),
'ReXXXXXXX' => Power_BI_App_Report_Details__mdt.getInstance('Retail_MS_C3'),
'MarXXXXXXXshboard' => Power_BI_App_Report_Details__mdt.getInstance('XXXXXXX_Summary_Dashboard'),
'MarXXXXXXXashboard' => Power_BI_App_Report_Details__mdt.getInstance('XXXXXXXd_County_Dashboard'),
'MaXXXXXXXhboardAG' => Power_BI_App_Report_Details__mdt.getInstance('XXXXXXXmary_DashboardAG'),
'MarXXXXXXXardCE' => Power_BI_App_Report_Details__mdt.getInstance('XXXXXXXmary_DashboardCE'),
'PuXXXXXXXect' => Power_BI_App_Report_Details__mdt.getInstance('Purchase_Prospect')
};
System.debug('Determining workspace and report IDs based on inputs...');
if (String.isNotBlank(selectedFlexiPage) && reportDetailsMap.containsKey(selectedFlexiPage)) {
workspaceId = reportDetailsMap.get(selectedFlexiPage).GroupId__c;
reportId = reportDetailsMap.get(selectedFlexiPage).ReportId__c;
} else if (String.isNotBlank(mystateValue) && reportDetailsMap.containsKey(mystateValue)) {
workspaceId = reportDetailsMap.get(mystateValue).GroupId__c;
reportId = reportDetailsMap.get(mystateValue).ReportId__c;
if (mystateValue == 'PurXXXect') {
isPurchaseProspect = true;
}
}
System.debug('WorkspaceId: ' + workspaceId + ', ReportId: ' + reportId);
if (String.isBlank(workspaceId) || String.isBlank(reportId)) {
throw new AuraHandledException('Invalid inputs or no matching metadata found.');
}
String accessToken = getPowerBiAccessToken();
System.debug('Successfully fetched access token.');
PowerBiReport report = getPowerBiReportDetails(workspaceId, reportId, accessToken);
System.debug('Power BI report details retrieved: ' + report);
PowerBiEmbedToken embedToken = generateEmbedToken(report, workspaceId, accessToken, isPurchaseProspect);
System.debug('Embed token generated successfully.');
reportData.workspaceId = workspaceId;
reportData.reportId = reportId;
reportData.name = report.name;
reportData.embedUrl = report.embedUrl;
reportData.embedToken = embedToken.token;
reportData.embedTokenExpires = embedToken.expiration;
} catch (Exception e) {
System.debug('Error occurred: ' + e.getMessage());
reportData.error = e.getMessage();
}
return reportData;
}
@TestVisible
private static String getPowerBiAccessToken() {
System.debug('Fetching Power BI access token...');
try {
Power_BI_App_Credentials__mdt credentials = Power_BI_App_Credentials__mdt.getInstance('Power_BI_Credentials');
String aadTokenEndpoint = 'https://login.microsoftonline.com/' + credentials.TenantId__c + '/oauth2/v2.0/token';
HttpRequest req = new HttpRequest();
req.setMethod('POST');
req.setEndpoint(aadTokenEndpoint);
req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
req.setTimeout(120000);
req.setBody(new ClientCredentialPostData(
credentials.ClientId__c,
'1',
credentials.ClientSecret__c,
credentials.Scope__c,
credentials.Grant_Type__c
).getPostData());
HttpResponse response = new Http().send(req);
System.debug('Access token response: ' + response.getBody());
if (response.getStatusCode() == 200) {
return ((ClientCredentialResponse) JSON.deserialize(response.getBody(), ClientCredentialResponse.class)).access_token;
} else {
throw new AuraHandledException('Failed to get access token. Response status: ' + response.getStatus());
}
} catch (Exception e) {
System.debug('Error fetching access token: ' + e.getMessage());
throw new AuraHandledException('Error fetching access token: ' + e.getMessage());
}
}
@TestVisible
private static PowerBiReport getPowerBiReportDetails(String workspaceId, String reportId, String accessToken) {
System.debug('Fetching Power BI report details for workspaceId: ' + workspaceId + ', reportId: ' + reportId);
try {
Power_BI_App_Credentials__mdt credentials = Power_BI_App_Credentials__mdt.getInstance('Power_BI_Credentials');
String endPointUrl = credentials.Endpoint_Request__c + 'groups/' + workspaceId + '/reports/' + reportId;
HttpRequest req = new HttpRequest();
req.setMethod('GET');
req.setEndpoint(endPointUrl);
req.setHeader('Authorization', 'Bearer ' + accessToken);
req.setTimeout(120000);
HttpResponse response = new Http().send(req);
System.debug('Report details response: ' + response.getBody());
if (response.getStatusCode() == 200) {
return (PowerBiReport) JSON.deserialize(response.getBody(), PowerBiReport.class);
} else {
throw new AuraHandledException('Error fetching report details. Response status: ' + response.getStatus());
}
} catch (Exception e) {
System.debug('Error fetching Power BI report details: ' + e.getMessage());
throw new AuraHandledException('Error fetching Power BI report details: ' + e.getMessage());
}
}
@TestVisible
private static PowerBiEmbedToken generateEmbedToken(PowerBiReport report, String workspaceId, String accessToken, Boolean isPurchaseProspect) {
System.debug('Generating embed token...');
try {
Power_BI_App_Credentials__mdt credentials = Power_BI_App_Credentials__mdt.getInstance('Power_BI_Credentials');
EmbedTokenRequest embedRequest = new EmbedTokenRequest();
embedRequest.accessLevel = 'View';
embedRequest.datasets = new List<DatasetWrapper>{ new DatasetWrapper(report.datasetId) };
embedRequest.reports = new List<ReportWrapper>{ new ReportWrapper(report.id) };
embedRequest.targetWorkspaces = new List<WorkspaceWrapper>{ new WorkspaceWrapper(workspaceId) };
if (!isPurchaseProspect) {
User currentUser = [SELECT FederationIdentifier FROM User WHERE Id = :UserInfo.getUserId() LIMIT 1];
EffectiveIdentity identity = new EffectiveIdentity(currentUser.FederationIdentifier, report.datasetId, report.id);
embedRequest.identities = new List<EffectiveIdentity>{ identity };
}
String endPointUrl = credentials.Endpoint_Request__c + 'groups/' + workspaceId + '/reports/' + report.id + '/GenerateToken';
HttpRequest req = new HttpRequest();
req.setMethod('POST');
req.setEndpoint(endPointUrl);
req.setHeader('Authorization', 'Bearer ' + accessToken);
req.setHeader('Content-Type', 'application/json');
req.setTimeout(120000);
req.setBody(JSON.serialize(embedRequest));
HttpResponse response = new Http().send(req);
System.debug('Embed token response: ' + response.getBody());
if (response.getStatusCode() == 200) {
return (PowerBiEmbedToken) JSON.deserialize(response.getBody(), PowerBiEmbedToken.class);
} else {
throw new AuraHandledException('Error generating embed token. Response status: ' + response.getStatus());
}
} catch (Exception e) {
System.debug('Error generating embed token: ' + e.getMessage());
throw new AuraHandledException('Error generating embed token: ' + e.getMessage());
}
}
// ---------------- INNER CLASSES ------------------
public class ClientCredentialPostData {
public String client_id;
public String client_info;
public String client_secret;
public String scope;
public String grant_type;
public ClientCredentialPostData(String client_id, String client_info, String client_secret, String scope, String grant_type) {
this.client_id = client_id;
this.client_info = client_info;
this.client_secret = client_secret;
this.scope = scope;
this.grant_type = grant_type;
}
public String getPostData() {
return 'client_id=' + this.client_id +
'&client_info=' + this.client_info +
'&client_secret=' + this.client_secret +
'&scope=' + this.scope +
'&grant_type=' + this.grant_type;
}
}
public class EffectiveIdentity {
public String username;
public List<String> datasets;
public List<String> roles = new List<String>{ 'Admin', 'By e-mail', 'Users'};
public List<String> reports;
public EffectiveIdentity(String username, String datasetId, String reportId) {
this.username = username;
this.datasets = new List<String>{ datasetId };
this.reports = new List<String>{ reportId };
}
}
public class PowerBiReport {
public String id;
public String reportType;
public String name;
public String webUrl;
public String embedUrl;
public boolean isFromPbix;
public boolean isOwnedByMe;
public String datasetId;
}
public class PowerBiEmbedToken {
public String token;
public String tokenId;
public DateTime expiration;
}
public class PowerBiReportData {
@AuraEnabled public String workspaceId;
@AuraEnabled public String reportId;
@AuraEnabled public String name;
@AuraEnabled public String embedUrl;
@AuraEnabled public String embedToken;
@AuraEnabled public DateTime embedTokenExpires;
@AuraEnabled public String error;
}
public class EmbedTokenRequest {
public List<EffectiveIdentity> identities;
public List<DatasetWrapper> datasets;
public List<ReportWrapper> reports;
public List<WorkspaceWrapper> targetWorkspaces;
public String accessLevel;
}
public class DatasetWrapper {
public String id;
public DatasetWrapper(String id) {
this.id = id;
}
}
public class ReportWrapper {
public String id;
public ReportWrapper(String id) {
this.id = id;
}
}
public class WorkspaceWrapper {
public String id;
public WorkspaceWrapper(String id) {
this.id = id;
}
}
public class ClientCredentialResponse {
public String access_token;
public String expires_in;
public String ext_expires_in;
public String token_type;
}
}
@isTest
public class PowerBiEmbedManager_Test {
@isTest
static void testGetEmbeddingDataForReport_ValidInput() {
Test.startTest();
HttpCalloutMock mock = new PowerBiHttpMock();
Test.setMock(HttpCalloutMock.class, mock);
PowerBiEmbedManager.PowerBiReportData result = PowerBiEmbedManager.getEmbeddingDataForReport('Retail MS C1', '');
Test.stopTest();
}
@isTest
static void testGetEmbeddingDataForReport_InvalidInput() {
Test.startTest();
PowerBiEmbedManager.PowerBiReportData result = PowerBiEmbedManager.getEmbeddingDataForReport('InvalidPage', 'Market_Share_Dealer_Summary_Dashboard');
System.assertNotEquals(null, result.error, 'Error message should be returned for invalid input');
Test.stopTest();
}
@isTest
static void testGetPowerBiAccessToken() {
Test.startTest();
HttpCalloutMock mock = new PowerBiHttpMock();
Test.setMock(HttpCalloutMock.class, mock);
String token = PowerBiEmbedManager.getPowerBiAccessToken();
System.assertNotEquals(null, token, 'Access token should be returned');
Test.stopTest();
}
@isTest
static void testGetPowerBiReportDetails() {
Test.startTest();
HttpCalloutMock mock = new PowerBiHttpMock();
Test.setMock(HttpCalloutMock.class, mock);
String testWorkspaceId = 'testWorkspaceId1';
String testReportId = 'testReportId1';
String testToken = 'testAccessToken';
PowerBiEmbedManager.PowerBiReport report = PowerBiEmbedManager.getPowerBiReportDetails(testWorkspaceId, testReportId, testToken);
System.assertNotEquals(null, report, 'Report should not be null');
System.assertEquals('testReportId1', report.id, 'Report ID should match');
Test.stopTest();
}
@isTest
static void testGenerateEmbedToken() {
Test.startTest();
HttpCalloutMock mock = new PowerBiHttpMock();
Test.setMock(HttpCalloutMock.class, mock);
PowerBiEmbedManager.PowerBiReport report = new PowerBiEmbedManager.PowerBiReport();
report.id = 'testReportId';
report.datasetId = 'testDatasetId';
PowerBiEmbedManager.PowerBiEmbedToken embedToken = PowerBiEmbedManager.generateEmbedToken(report, 'testWorkspaceId1', 'testAccessToken', false);
Test.stopTest();
}
private class PowerBiHttpMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
res.setStatusCode(200);
if (req.getEndpoint().contains('token')) {
res.setBody('{"access_token": "testAccessToken"}');
} else if (req.getEndpoint().contains('reports')) {
res.setBody('{"id": "testReportId1", "name": "Test Report", "embedUrl": "https://app.powerbi.com/"}');
} else if (req.getEndpoint().contains('GenerateToken')) {
res.setBody('{"token": "testEmbedToken", "expiration": "2025-12-31T23:59:59"}');
}
return res;
}
}
}
//-------------------------------------------------------------------
Important Note -
1. PowerBI Remote Site Settings - https://api.powerbi.com
2. powerbijs (Static Resource URL) - https://raw.githubusercontent.com/Microsoft/PowerBI-JavaScript/refs/heads/master/dist/powerbi.js
3. SalesforceGlobalHeaderHide (Static Resource .CSS File Code) -
.slds-context-bar{
display:none !important;
}
.slds-global-header{
display:none !important;
}
The implementation of Power BI in Salesforce resulted in: ✅ Seamless Power BI access without additional logins. ✅ Embedded reports within Salesforce account and home pages. ✅ Dynamic filtering of reports based on user selection. ✅ Enhanced analytics capabilities within the Salesforce ecosystem.
Integrating Power BI with Salesforce unlocks powerful reporting capabilities, enhancing data visualization and decision-making. By leveraging SSO, LWC, Apex, and Power BI APIs, organizations can deliver a seamless reporting experience within Salesforc
Comments
Post a Comment