Angular — Power BI Client Integration

Power BI Logo

What is Power BI?

Power BI is a business analytics tool developed by Microsoft. It provides interactive visualizations and business intelligence capabilities with an interface that is user-friendly and accessible to both technical and non-technical users. Power BI allows users to connect to various data sources, transform and model the data, and create interactive reports and dashboards.

Power BI Usage Scenarios

There are two scenarios: Embed for your customers and Embed for your organization. We’ll be covering Embed for Your Customers scenario.

Embed For Your Customers

In this scenario, your customer does not need any license of Power BI or data. You or your organization own the license and data.

The custom application requires an embedding identity that has permission and an appropriate license to access Power BI content.

Embed for Your Customers

That might look a bit complicated, but I got you.

First, your customers authenticate with the app. They way you authenticate them is not important. It doesn’t have to be Azure AD.

Second, you need to provide an Azure AD Access Token that has permissions/license to Power BI service. An embedding identity can be a service principal or a master user account. We are not going into that because It’s a back-end side of things.

We’re going to use this token to fetch Metadata includes properties required to embed content in the custom application from Power BI Rest API.

Embed For Your Organization

In this scenario, users need the license of Power BI and data.

Embed for Your Organization

Only difference here is that app users authenticate with Azure AD.

Enough talk. Let’s get started.

Implementation

We need to install powerbi-client and powerbi-client-angular:

Don’t forget to import PowerBIEmbedModule in your module.

npm i powerbi-client powerbi-client-angular

Then, we should get Azure AD Access Token. Since we follow Embed for Your Customers scenario, we’ll receive our token from our API. If you are following Embed for Your Organization, you’ll already had that token.

getPowerBiToken(): Observable<IPowerBiTokenResponse> {
    return this.http
      .get<IPowerBiTokenResponse>("your endpoint to get the token")
      .pipe(
        retry(5),
        catchError((err) => {
          console.log("Error occured while getting power bi token");
          console.log(err);
          return throwError(err);
        })
      );

This token should be persisted because we’re going to use this token for Power BI Rest API.

At that point, we’re going to use Power BI Rest API to get Metadata includes properties required to embed content.

What Are the Required Properties to Embed Content?

{
  type: "report",
  id: configDto.id,
  embedUrl: configDto.embedUrl,
  accessToken: configDto.accessToken,
  tokenType: models.TokenType.Embed,
}

id is the id of the report, embedUrl is the embedUrl of the report, accessToken is embed token (we will get there soon) and lastly, tokenType is either Aad or Embed (if you are following Embed for Your Organization you may need to use Aad). There are a lot more properties, but these are the required ones. You can access those properties by importing IEmbedConfiguration from powerbi-client.

How To Get Metadata?

In order to get the metadata, first, you need to know the id of the report you want to embed.

You can use this endpoint to receive available reports. Don’t forget to include Azure AD Access Token in headers as Authorization. If you have the report id, then you can use this endpoint to get its metadata.

getReport(reportId: string) {
    return this.http.get<IPowerBiReportResponse>(`${environment.powerBIRestAPIUrl}reports/${reportId}`)
}

I have an interceptor that appends my Azure AD Access Token to the headers of the request and also gets a new one if It is expired.

@Injectable()
export class PowerBiRestAPIInterceptor implements HttpInterceptor {
  constructor(private readonly http: HttpClient) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    if (request.url.indexOf('api.powerbi.com/v1.0/myorg') > -1) {
      const token = localStorage.getItem('azure-ad-access-token');
      const newRequest: HttpRequest<any> = request.clone({
        headers: request.headers.set('Authorization', 'Bearer ' + token)
      });
      return next.handle(newRequest).pipe(
        catchError((error: HttpErrorResponse) => {
          if (error.status === 403) {
            return this.http.get<IPowerBiTokenResponse>(environment.backendAPIUrl + '/api/azure-ad-access-token').pipe(
              retry(5),
              mergeMap((token) => {
                if (token) {
                  localStorage.setItem('azure-ad-access-token', token.access_token);
                  const newHeaders = request.headers.set('Authorization', `Bearer ${token.access_token}`);
                  const newRequest = request.clone({ headers: newHeaders });
                  return next.handle(newRequest);
                }
              })
            );
          }
        })
      );
    }
    return next.handle(request);
  }
}

You don’t actually need an interceptor, but I preferred that.

{
  "datasetId": "cfafbeb1-8037-4d0c-896e-a46fb27ff229",
  "id": "5b218778-e7a5-4d73-8187-f10824047715",
  "name": "SalesMarketing",
  "webUrl": "https://app.powerbi.com//reports/5b218778-e7a5-4d73-8187-f10824047715",
  "embedUrl": "https://app.powerbi.com/reportEmbed?reportId=5b218778-e7a5-4d73-8187-f10824047715"
}

That’s the metadata we were talking about! Now, we need to get an embed token for this report.

How To Get Embed Token for Reports

We’ll use generate token endpoint of Power BI Rest API to get an embed token for a report/ reports.

There is a limit:

  • Maximum 50 datasets.
  • Maximum 50 target workspaces.
  • Maximum 50 reports.
  • Can be used to generate an embed token in one request.
getEmbedToken(datasetId: string, reportId: string) {
    const body = {
      datasets: [
        {
          id: datasetId
        }
      ],
      reports: [
        {
          id: reportId
        }
      ]
    }
    return this.http.post<IPowerBiEmbedTokenResponse>(`${environment.powerBIRestAPIUrl}GenerateToken`, body)
}

We have all the required values to embed our report.

How to Embed the Report?

<powerbi-report
  [embedConfig]="getPowerBIEmbedConfig(configDto)"
  [cssClassName]="reportClass"
  [phasedEmbedding]="false"
  [eventHandlers]="eventHandlersMap"
  *ngIf="configDto$ | async as configDto"
>
</powerbi-report>
getPowerBIEmbedConfig(configDto: IPowerBiConfigDto): IEmbedConfiguration {
    return {
      type: "report",
      id: configDto.id,
      embedUrl: configDto.embedUrl,
      accessToken: configDto.accessToken,
      tokenType: models.TokenType.Embed,
      settings: {
        background: models.BackgroundType.Transparent,
      }
    }
}

getPowerBIEmbedConfig() maps the result I gathered from the Power BI Rest API into IEmbedConfiguration type.

Sample report for testing. You can make a HTTP get request to this endpoint and receive an example result in order to use and test for embedding. It returns all required parameters.

Also, Microsoft has a demo application.

How to Refresh Embed Token

There is a way to automatically refresh token if you’re following Embed for Your Organization scenario. Both Azure AD Access Token and Embed Token has an expire time. In order to get a new Embed Token, we need to use Azure AD Access Token. If Azure AD Access Token is expired, then we can’t refresh our Embed Token. First things first, let’s see how to refresh our Azure AD Access Token

I have an interceptor for refreshing my Azure AD Access Token. It returns me 403 HTTP Error Code if my token is expired when I make a call to generate a new Embed Token. I can catch this error and make a HTTP call to my application’s backend to receive an Azure AD Access Token.

@Injectable()
export class PowerBiRestAPIInterceptor implements HttpInterceptor {
  constructor(private readonly http: HttpClient) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    if (request.url.indexOf('api.powerbi.com/v1.0/myorg') > -1) {
      const token = localStorage.getItem('azure-ad-access-token');
      const newRequest: HttpRequest<any> = request.clone({
        headers: request.headers.set('Authorization', 'Bearer ' + token)
      });
      return next.handle(newRequest).pipe(
        catchError((error: HttpErrorResponse) => {
          if (error.status === 403) {
            return this.http.get<IPowerBiTokenResponse>(environment.backendAPIUrl + '/api/azure-ad-access-token').pipe(
              retry(5),
              mergeMap((token) => {
                if (token) {
                  localStorage.setItem('azure-ad-access-token', token.access_token);
                  const newHeaders = request.headers.set('Authorization', `Bearer ${token.access_token}`);
                  const newRequest = request.clone({ headers: newHeaders });
                  return next.handle(newRequest);
                }
              })
            );
          }
        })
      );
    }
    return next.handle(request);
  }
}

For Embed Token, unfortunately, we cannot use interceptor because Power BI’s iframe uses this token to make HTTP requests. The way Microsoft recommends is that frequently check if the token is expired and if it is, refresh it.

source = timer(undefined, 30000);

private refreshPowerBItokens(reportId: string, datasetId: string) {
    const MINUTES_BEFORE_EXPIRATION = 10;

    const subscription = this.source.subscribe(_ =>
      this.checkTokenAndUpdate(MINUTES_BEFORE_EXPIRATION, reportId, datasetId));

    this.subscriptions.push(subscription);
  }

  private checkTokenAndUpdate(MINUTES_BEFORE_EXPIRATION: number, reportId: string, datasetId: string) {
    const tokenExpiration = localStorage.getItem("embed_token_exp");

    const currentTime = Date.now();
    const expiration = Date.parse(tokenExpiration);

    const timeUntilExpiration = expiration - currentTime;
    const timeToUpdate = MINUTES_BEFORE_EXPIRATION * 60 * 1000;

    if (timeUntilExpiration <= timeToUpdate) {
      this.updateToken(reportId, datasetId);
    }
  }

  private async updateToken(reportId: string, datasetId: string) {
    const powerBIToken = await this.powerBIService.getPowerBiToken(reportId, {}).toPromise()
    localStorage.setItem("azure-ad-access-token", powerBIToken.access_token)

    const embedToken = await this.powerBIService.getEmbedToken(datasetId, reportId).toPromise()
    const date = new Date(embedToken.expiration);
    const locale = date.toString()
    localStorage.setItem("embed_token_exp", locale)

    await this.reportObj.getReport().setAccessToken(embedToken.token)
  }

That’s it! Thanks for reading. If you have any question, please leave a comment. I’d love to answer.