Codeunit 6301 Power BI Service Mgt.

App
Base Application
Namespace
System.Integration.PowerBI
Versions
17-28

Procedures, 63Events, 4Obsolete, 43

Versions171819202122232425262728

Source29

Source in 29

src/Layers/W1/BaseApp/Modules/System/PowerBI/PowerBIServiceMgt.Codeunit.al340 lines, Copyright (c) Microsoft Corporation. MIT

namespace System.Integration.PowerBI;

using System;
using System.Azure.Identity;
using System.Environment.Configuration;
using System.Integration;
using System.Reflection;
using System.Security.User;
using System.Telemetry;
using System.Threading;

codeunit 6301 "Power BI Service Mgt."
{
    var
        AzureAdMgt: Codeunit "Azure AD Mgt.";
        GenericErr: Label 'An error occurred while trying to get reports from the Power BI service. Please try again or contact your system administrator if the error persists.';
        PowerBiResourceNameTxt: Label 'Power BI Services';
#if not CLEAN27
        MainPageRatioTxt: Label '16:9', Locked = true;
        FactboxRatioTxt: Label '4:3', Locked = true;
#endif
        FailedAuthErr: Label 'We failed to authenticate with Power BI. Try to sign out and in again. This problem typically happens if you no longer have a license for Power BI or if you just changed your email or password.';
        UnauthorizedErr: Label 'You do not have a Power BI account. If you have just activated a license, it might take several minutes for the changes to be effective in Power BI.';
        PowerBIEmbedReportUrlTemplateTxt: Label 'https://app.powerbi.com/reportEmbed?reportId=%1', Locked = true;
        Dyn365AppSourceUrlTxt: Label 'https://go.microsoft.com/fwlink/?linkid=862352', Locked = true;
        PowerBIMyOrgUrlTxt: Label 'https://go.microsoft.com/fwlink/?linkid=862353', Locked = true;
        JobQueueCategoryCodeTxt: Label 'PBI EMBED', Locked = true;
        JobQueueCategoryDescriptionTxt: Label 'Synchronize Power BI reports', MaxLength = 30;
        // Telemetry constants
        EmbedCorrelationTelemetryTxt: Label 'Embed element started with type: %1, and correlation: %2', Locked = true;
        EmbedErrorOccurredTelemetryTxt: Label 'Embed error occurred with category: %1', Locked = true;
        PowerBiEmbedFeatureTelemetryTok: Label 'Power BI Embed', Locked = true;
        PowerBiLicenseCheckErrorTelemetryMsg: Label 'Power BI license check finished with error.', Locked = true;
        PowerBiLicenseCheckSuccessTelemetryMsg: Label 'Power BI license check returned success.', Locked = true;
        PowerBiTelemetryCategoryLbl: Label 'AL Power BI Embedded', Locked = true;
        EmptyAccessTokenTelemetryMsg: Label 'Encountered an empty access token.', Locked = true;
        ScheduleSyncTelemetryMsg: Label 'Scheduling sync for UTC datetime: %1.', Locked = true;

    [Scope('OnPrem')]
    procedure CheckForPowerBILicenseInForeground(): Boolean
    var
        PowerBIServiceProvider: Interface "Power BI Service Provider";
        OperationResult: DotNet OperationResult;
    begin
        CreateServiceProvider(PowerBIServiceProvider);

        PowerBIServiceProvider.CheckUserLicense(OperationResult);

        if OperationResult.Successful then
            Session.LogMessage('0000C0H', PowerBiLicenseCheckSuccessTelemetryMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PowerBiTelemetryCategoryLbl)
        else
            Session.LogMessage('0000B6Y', PowerBiLicenseCheckErrorTelemetryMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PowerBiTelemetryCategoryLbl);

        exit(OperationResult.Successful);
    end;

    [Scope('OnPrem')]
    procedure IsUserReadyForPowerBI(): Boolean
    begin
        if not AzureAdMgt.IsAzureADAppSetupDone() then
            exit(false);

        exit(not AzureAdMgt.GetAccessTokenAsSecretText(GetPowerBIResourceUrl(), GetPowerBiResourceName(), false).IsEmpty());
    end;

    procedure GetPowerBIResourceUrl(): Text
    var
        PowerBIUrlMgt: Codeunit "Power BI Url Mgt";
    begin
        exit(PowerBIUrlMgt.GetPowerBIResourceUrl());
    end;

    procedure GetPowerBiResourceName(): Text
    begin
        exit(PowerBiResourceNameTxt);
    end;

    procedure GetGenericError(): Text
    begin
        exit(GenericErr);
    end;

#if not CLEAN27
    [Obsolete('This function is now deprecated, and the client decides the addin ratio instead.', '27.0')]
    procedure GetFactboxRatio(): Text
    begin
        exit(FactboxRatioTxt);
    end;

    [Obsolete('This function is now deprecated, and the client decides the addin ratio instead.', '27.0')]
    procedure GetMainPageRatio(): Text
    begin
        exit(MainPageRatioTxt);
    end;
#endif

    procedure GetContentPacksServicesUrl(): Text
    begin
        // Gets the URL for AppSource's list of content packs, like Power BI's Services button, filtered to Dynamics reports.
        exit(Dyn365AppSourceUrlTxt);
    end;

    procedure GetContentPacksMyOrganizationUrl(): Text
    begin
        // Gets the URL for Power BI's embedded AppSource page listing reports shared by the user's organization.
        exit(PowerBIMyOrgUrlTxt);
    end;

    procedure SynchronizeReportsInBackground(Context: Text[50])
    var
        JobQueueEntry: Record "Job Queue Entry";
        ScheduledDateTime: DateTime;
    begin
        ScheduledDateTime := CurrentDateTime();

        Session.LogMessage('0000FB2', StrSubstNo(ScheduleSyncTelemetryMsg, Format(ScheduledDateTime, 50, 9)), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PowerBiTelemetryCategoryLbl);
        JobQueueEntry.ScheduleJobQueueEntryForLater(Codeunit::"Power BI Report Synchronizer", ScheduledDateTime, GetJobQueueCategoryCode(), Context)
    end;

    [Scope('OnPrem')]
    procedure IsUserSynchronizingReports(): Boolean
    var
        JobQueueEntry: Record "Job Queue Entry";
    begin
        JobQueueEntry.SetRange("User ID", UserId());
        JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit);
        JobQueueEntry.SetRange("Object ID to Run", Codeunit::"Power BI Report Synchronizer");
        JobQueueEntry.SetFilter(Status, '%1|%2|%3|%4', JobQueueEntry.Status::Ready, JobQueueEntry.Status::"In Process", JobQueueEntry.Status::"On Hold", JobQueueEntry.Status::"On Hold with Inactivity Timeout");

        if not JobQueueEntry.IsEmpty() then
            exit(true);

        exit(false);
    end;

    [Scope('OnPrem')]
    procedure GetReportsUrl(): Text
    var
        PowerBIUrlMgt: Codeunit "Power BI Url Mgt";
    begin
        exit(PowerBIUrlMgt.GetPowerBIReportsUrl());
    end;

    [Scope('OnPrem')]
    procedure GetEnglishContext(): Code[30]
    var
        AllProfile: Record "All Profile";
        ConfPersonalizationMgt: Codeunit "Conf./Personalization Mgt.";
    begin
        // Returns an English profile ID for the Report Selection
        ConfPersonalizationMgt.GetCurrentProfileNoError(AllProfile);
        exit(AllProfile."Profile ID");
    end;

    /// <summary>
    /// Add a Power BI report visual to the database, so that it's displayed in a certain context for the current user.
    /// </summary>
    /// <param name="ReportId">The ID of the Power BI Report that contains the visual to embed</param>
    /// <param name="ReportPageId">The name of the page in the report that contains the visual to embed</param>
    /// <param name="ReportVisualId">The ID of the report visual to embed</param>
    /// <param name="Context">The context where the Power BI report visual should show up</param>
    /// <remarks>
    /// The easiest way to get the necessary IDs for report visuals is to:
    ///   1. Open the Power BI report in the browser
    ///   2. Hover over the visual you want to embed, and click on the three dots menu
    ///   3. Choose to "Share" the visual, and choose "Link to this Visual"
    ///   4. Use the "Copy" button to copy the URL
    ///   5. From the URL, you can find:
    ///     a. The Report ID after the /reports/ segment 
    ///     b. The Report Page right after the Report ID
    ///     c. The visual ID in a URL query parameter called "visual"
    ///
    /// Example URL with placeholders:
    /// https://app.powerbi.com/groups/me/reports/REPORT_ID/PAGE_ID?[...]&amp;visual=VISUAL_ID
    /// </remarks>
    procedure AddReportVisualForContext(ReportId: Guid; ReportPageId: Text[200]; ReportVisualId: Text[200]; Context: Text[50])
    var
        PowerBIContextSettings: Record "Power BI Context Settings";
        PowerBIDisplayedElement: Record "Power BI Displayed Element";
    begin
        if not PowerBIDisplayedElement.Get(UserSecurityId(), Context, PowerBIDisplayedElement.MakeReportVisualKey(ReportId, ReportPageId, ReportVisualId), PowerBIDisplayedElement.ElementType::"Report Visual") then begin
            PowerBIDisplayedElement.Init();
            PowerBIDisplayedElement.ElementType := PowerBIDisplayedElement.ElementType::"Report Visual";
            PowerBIDisplayedElement.ElementId := PowerBIDisplayedElement.MakeReportVisualKey(ReportId, ReportPageId, ReportVisualId);
            // NOTE: The Power BI team recommends to get the embed URL from the Power BI REST APIs, as the URL format might change in the future. 
            // However, currently the approach below is also supported.
            PowerBIDisplayedElement.ElementEmbedUrl := StrSubstNo(PowerBIEmbedReportUrlTemplateTxt, ReportId);
            PowerBIDisplayedElement.Context := Context;
            PowerBIDisplayedElement.UserSID := UserSecurityId();
            PowerBIDisplayedElement.ShowPanesInExpandedMode := true;
            PowerBIDisplayedElement.ShowPanesInNormalMode := false;
            PowerBIDisplayedElement.Insert(true);
        end;

        PowerBIContextSettings.CreateOrReadForCurrentUser(Context);
        if not PowerBIContextSettings.LockToSelectedElement then begin
            PowerBIContextSettings.LockToSelectedElement := true;
            PowerBIContextSettings.Modify(true);
        end;
    end;

    /// <summary>
    /// Add a Power BI report to the database, so that it's displayed in a certain context for the current user.
    /// </summary>
    /// <param name="ReportId">The ID of the Power BI Report to embed</param>
    /// <param name="Context">The context where the Power BI report should show up</param>
    procedure AddReportForContext(ReportId: Guid; Context: Text[50])
    var
        PowerBIContextSettings: Record "Power BI Context Settings";
        PowerBIDisplayedElement: Record "Power BI Displayed Element";
    begin
        if not PowerBIDisplayedElement.Get(UserSecurityId(), Context, PowerBIDisplayedElement.MakeReportKey(ReportId), PowerBIDisplayedElement.ElementType::"Report") then begin
            PowerBIDisplayedElement.Init();
            PowerBIDisplayedElement.ElementType := PowerBIDisplayedElement.ElementType::"Report";
            PowerBIDisplayedElement.ElementId := PowerBIDisplayedElement.MakeReportKey(ReportId);
            // NOTE: The Power BI team recommends to get the embed URL from the Power BI REST APIs, as the URL format might change in the future. 
            // However, currently the approach below is also supported.
            PowerBIDisplayedElement.ElementEmbedUrl := StrSubstNo(PowerBIEmbedReportUrlTemplateTxt, ReportId);
            PowerBIDisplayedElement.Context := Context;
            PowerBIDisplayedElement.UserSID := UserSecurityId();
            PowerBIDisplayedElement.ShowPanesInExpandedMode := true;
            PowerBIDisplayedElement.ShowPanesInNormalMode := false;
            PowerBIDisplayedElement.Insert(true);
        end;

        PowerBIContextSettings.CreateOrReadForCurrentUser(Context);
        if not PowerBIContextSettings.LockToSelectedElement then begin
            PowerBIContextSettings.LockToSelectedElement := true;
            PowerBIContextSettings.Modify(true);
        end;
    end;

    procedure IsUserAdminForPowerBI(UserSecurityId: Guid): Boolean
    var
        UserPermissions: Codeunit "User Permissions";
    begin
        exit(UserPermissions.IsSuper(UserSecurityId));
    end;

    procedure CheckPowerBITablePermissions(): Boolean
    var
        PowerBIBlob: Record "Power BI Blob";
        PowerBIDefaultSelection: Record "Power BI Default Selection";
        PowerBIContextSettings: Record "Power BI Context Settings";
        [SecurityFiltering(SecurityFilter::Ignored)]
        PowerBIContextSettings2: Record "Power BI Context Settings";
        PowerBICustomerReports: Record "Power BI Customer Reports";
        PowerBIDisplayedElement: Record "Power BI Displayed Element";
    begin
        exit(PowerBIBlob.ReadPermission()
            and PowerBIDefaultSelection.ReadPermission()
            and PowerBICustomerReports.ReadPermission()
            and PowerBIContextSettings2.WritePermission() and PowerBIContextSettings.ReadPermission()
            and PowerBIDisplayedElement.ReadPermission());
    end;

    procedure GetPowerBiTelemetryCategory(): Text
    begin
        exit(PowerBiTelemetryCategoryLbl);
    end;

    [Scope('OnPrem')]
    procedure GetJobQueueCategoryCode(): Code[10]
    var
        JobQueueCategory: Record "Job Queue Category";
    begin
        JobQueueCategory.InsertRec(
            CopyStr(JobQueueCategoryCodeTxt, 1, MaxStrLen(JobQueueCategory.Code)),
            CopyStr(JobQueueCategoryDescriptionTxt, 1, MaxStrLen(JobQueueCategory.Description)));

        exit(JobQueueCategory.Code);
    end;

    internal procedure GetPowerBiFeatureTelemetryName(): Text
    begin
        exit(PowerBiEmbedFeatureTelemetryTok);
    end;

    [NonDebuggable]
    procedure InitializeAddinToken(PowerBIManagement: ControlAddIn PowerBIManagement)
    var
        AccessToken: Text;
        HttpUtility: DotNet HttpUtility;
    begin
        AccessToken := HttpUtility.JavaScriptStringEncode(
            AzureAdMgt.GetAccessTokenAsSecretText(GetPowerBIResourceUrl(), GetPowerBiResourceName(), false).Unwrap()
            );

        if AccessToken = '' then begin
            Session.LogMessage('0000KQL', EmptyAccessTokenTelemetryMsg, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PowerBiTelemetryCategoryLbl);
            Error(FailedAuthErr);
        end;

        PowerBIManagement.SetToken(AccessToken);
    end;

    procedure LogVisualLoaded(CorrelationId: Text; EmbedType: Enum "Power BI Element Type")
    begin
        Session.LogMessage('0000KAF', StrSubstNo(EmbedCorrelationTelemetryTxt, EmbedType, CorrelationId),
            Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetPowerBiTelemetryCategory());
    end;

    procedure LogEmbedError(ErrorCategory: Text)
    var
        FeatureTelemetry: Codeunit "Feature Telemetry";
    begin
        FeatureTelemetry.LogError('0000L02', GetPowerBiFeatureTelemetryName(), ErrorCategory, 'Error loading Power BI visual');

        Session.LogMessage('0000KAE', StrSubstNo(EmbedErrorOccurredTelemetryTxt, ErrorCategory),
            Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', GetPowerBiTelemetryCategory());
    end;

    internal procedure CreateServiceProvider(var PowerBIServiceProvider: Interface "Power BI Service Provider")
    var
        PowerBIUrlMgt: Codeunit "Power BI Url Mgt";
        PowerBIRestServiceProvider: Codeunit "Power BI Rest Service Provider";
        AzureAccessToken: SecretText;
        Handled: Boolean;
    begin
        OnServiceProviderCreate(PowerBIServiceProvider, Handled);

        if Handled then
            exit;

        AzureAccessToken := AzureAdMgt.GetAccessTokenAsSecretText(GetPowerBIResourceUrl(), GetPowerBiResourceName(), false);

        if AzureAccessToken.IsEmpty() then begin
            Session.LogMessage('0000B62', EmptyAccessTokenTelemetryMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PowerBiTelemetryCategoryLbl);
            Error(UnauthorizedErr);
        end;

        PowerBIRestServiceProvider.Initialize(AzureAccessToken, PowerBIUrlMgt.GetPowerBIApiUrl());
        PowerBIServiceProvider := PowerBIRestServiceProvider;
    end;

    [InternalEvent(false)]
    local procedure OnServiceProviderCreate(var PowerBIServiceProvider: Interface "Power BI Service Provider"; var Handled: Boolean)
    begin
    end;
}