Codeunit 6334 Power BI Upload Step Runner, source in 29

Source29

src/Layers/W1/BaseApp/Modules/System/PowerBI/APIs/PowerBIUploadStepRunner.Codeunit.al235 lines, Copyright (c) Microsoft Corporation. MIT

namespace System.Integration.PowerBI;

using System;
using System.Azure.Identity;
using System.Environment;
using System.Utilities;

/// <summary>
/// Executes exactly one upload step for a single report within a Codeunit.Run() boundary,
/// isolating errors so the caller can capture failure details via GetLastErrorText().
/// The synchronizer owns the step sequence loop and calls this codeunit once per step.
/// </summary>
codeunit 6334 "Power BI Upload Step Runner"
{
    Access = Internal;

    var
        PowerBIServiceMgt: Codeunit "Power BI Service Mgt.";
        AzureAdMgt: Codeunit "Azure AD Mgt.";
        EnvironmentInformation: Codeunit "Environment Information";
        GlobalReport: Interface "Power BI Uploadable Report";
        GlobalUploadTracker: Interface "Power BI Upload Tracker";
        GlobalContext: Text[50];
        FinalReportName: Text;
        IsConfigured: Boolean;
        ReportEnvNameTxt: Label '%1 (%2 - %3)', Locked = true;
        StartingImportTelemetryMsg: Label 'Starting actual import for internal blob ID: %1.', Locked = true;
        StartRetrievingImportTelemetryMsg: Label 'Retrieving import id %1.', Locked = true;
        UpdatingDatasetParametersTelemetryMsg: Label 'Updating dataset %1 with company "%2" and environment "%3".', Locked = true;
        GettingDatasourceForDatasetTelemetryMsg: Label 'Getting datasource for dataset %1.', Locked = true;
        RefreshingDatasetTelemetryMsg: Label 'Refreshing dataset %1.', Locked = true;
        UpdatingParametersFailedTelemetryMsg: Label 'Updating report parameters failed,', Locked = true;
        EmptyAccessTokenTelemetryMsg: Label 'Encountered an empty access token.', Locked = true;
        NotConfiguredErr: Label 'Power BI Upload Step Runner was not configured before execution. Call Configure() first.', Locked = true;
        PowerBIImportRequestFailedErr: Label 'Power BI import request failed.';
        PowerBIImportRetrievalFailedErr: Label 'Power BI import retrieval failed.';
        PowerBIDatasetRefreshFailedErr: Label 'Power BI dataset refresh failed.';

    trigger OnRun()
    var
        PowerBIServiceProvider: Interface "Power BI Service Provider";
        BlobInStream: InStream;
    begin
        // Configure() must always be called before OnRun(). If this error fires, the caller has a bug.
        if not IsConfigured then
            Error(NotConfiguredErr);

        PowerBIServiceMgt.CreateServiceProvider(PowerBIServiceProvider);
        FinalReportName := MakeReportNameForUpload(GlobalReport.GetReportName(), EnvironmentInformation.GetEnvironmentName(), CompanyName());

        case GlobalUploadTracker.GetStatus() of
            Enum::"Power BI Upload Status"::NotStarted:
                begin
                    GlobalReport.GetStream(BlobInStream);
                    StartImport(PowerBIServiceProvider, BlobInStream, FinalReportName);
                end;
            Enum::"Power BI Upload Status"::ImportStarted:
                GetImport(PowerBIServiceProvider);
            Enum::"Power BI Upload Status"::ImportFinished:
                UpdateParameters(PowerBIServiceProvider);
            Enum::"Power BI Upload Status"::ParametersUpdated:
                RefreshDataset(PowerBIServiceProvider);
            Enum::"Power BI Upload Status"::DataRefreshed:
                begin
                    GlobalReport.FinalizeUpload(GlobalUploadTracker, GlobalContext);
                    GlobalUploadTracker.TransitionTo(Enum::"Power BI Upload Status"::Completed);
                end;
        end;

        GlobalUploadTracker.Save();
    end;

    internal procedure Configure(
        var NewReport: Interface "Power BI Uploadable Report";
        var NewUploadTracker: Interface "Power BI Upload Tracker";
        NewContext: Text[50])
    begin
        GlobalReport := NewReport;
        GlobalUploadTracker := NewUploadTracker;
        GlobalContext := NewContext;
        IsConfigured := true;
    end;

    local procedure StartImport(PowerBIServiceProvider: Interface "Power BI Service Provider"; BlobInStream: InStream; ReportName: Text)
    var
        OperationResult: DotNet OperationResult;
        ImportId: Guid;
        Overwrite: Boolean;
    begin
        Session.LogMessage('0000G1Y', StrSubstNo(StartingImportTelemetryMsg, GlobalReport.GetReportKey()), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PowerBIServiceMgt.GetPowerBiTelemetryCategory());

        Overwrite := GlobalUploadTracker.ShouldOverwrite(GlobalReport.GetReportVersion());

        PowerBIServiceProvider.StartImport(
            BlobInStream,
            ReportName,
            Overwrite,
            ImportId,
            OperationResult);

        if OperationResult.Successful then begin
            GlobalUploadTracker.TransitionTo(Enum::"Power BI Upload Status"::ImportStarted);
            GlobalUploadTracker.SetImportId(ImportId);
        end else
            if OperationResult.ShouldRetry then
                GlobalUploadTracker.ScheduleRetry(GetRetryAfterOrDefault(OperationResult.RetryAfter))
            else
                Error(PowerBIImportRequestFailedErr);
    end;

    local procedure GetImport(PowerBIServiceProvider: Interface "Power BI Service Provider")
    var
        OperationResult: DotNet OperationResult;
        ReturnedReport: DotNet ReturnedReport;
        ImportState: Text;
    begin
        Session.LogMessage('0000G1Z', StrSubstNo(StartRetrievingImportTelemetryMsg, GlobalUploadTracker.GetImportId()), Verbosity::Normal, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::ExtensionPublisher, 'Category', PowerBIServiceMgt.GetPowerBiTelemetryCategory());

        PowerBIServiceProvider.GetImport(GlobalUploadTracker.GetImportId(), ImportState, ReturnedReport, OperationResult);

        if OperationResult.Successful then begin
            GlobalUploadTracker.TransitionTo(Enum::"Power BI Upload Status"::ImportFinished);
            GlobalUploadTracker.SetImportResult(ReturnedReport.ReportId, ReturnedReport.EmbedUrl, ReturnedReport.DatasetId);
            GlobalUploadTracker.SetUploadedReportName(FinalReportName);
        end else
            if OperationResult.ShouldRetry then
                GlobalUploadTracker.ScheduleRetry(GetRetryAfterOrDefault(OperationResult.RetryAfter))
            else
                Error(PowerBIImportRetrievalFailedErr);
    end;

    local procedure UpdateParameters(PowerBIServiceProvider: Interface "Power BI Service Provider")
    var
        UrlHelper: Codeunit "Url Helper";
        BusinessCentralAccessToken: SecretText;
        NewParameters: Dictionary of [Text, Text];
        OperationResult: DotNet OperationResult;
        GatewayId: Guid;
        DataSourceId: Guid;
        DatasetId: Text;
        EnvironmentValue: Text;
        CompanyValue: Text;
    begin
        DatasetId := GlobalUploadTracker.GetDatasetId();
        NewParameters := GlobalReport.GetDatasetParameters();

        // Update company and environment parameters
        Session.LogMessage('0000G20', StrSubstNo(UpdatingDatasetParametersTelemetryMsg, DatasetId, CompanyValue, EnvironmentValue), Verbosity::Normal,
            DataClassification::OrganizationIdentifiableInformation, TelemetryScope::ExtensionPublisher, 'Category', PowerBIServiceMgt.GetPowerBiTelemetryCategory());

        PowerBIServiceProvider.UpdateDatasetParameters(DatasetId, NewParameters, OperationResult);

        if (not OperationResult.Successful) and OperationResult.ShouldRetry then begin
            GlobalUploadTracker.ScheduleRetry(GetRetryAfterOrDefault(OperationResult.RetryAfter));
            Session.LogMessage('0000I20', UpdatingParametersFailedTelemetryMsg, Verbosity::Normal, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::ExtensionPublisher, 'Category', PowerBIServiceMgt.GetPowerBiTelemetryCategory());
            exit;
        end; // If it fails and we should not retry, we should ignore the step and try to go ahead (e.g. for custom uploaded reports)

        BusinessCentralAccessToken := AzureAdMgt.GetAccessTokenAsSecretText(UrlHelper.GetFixedEndpointWebServiceUrl(), '', false);
        if BusinessCentralAccessToken.IsEmpty() then begin
            Session.LogMessage('0000B63', EmptyAccessTokenTelemetryMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PowerBIServiceMgt.GetPowerBiTelemetryCategory());
            GlobalUploadTracker.TransitionTo(Enum::"Power BI Upload Status"::ParametersUpdated);
            exit;
        end;

        // Get datasource to update
        Clear(OperationResult);

        PowerBIServiceProvider.GetDatasource(DatasetId, DataSourceId, GatewayId, OperationResult);
        Session.LogMessage('0000G21', StrSubstNo(GettingDatasourceForDatasetTelemetryMsg, DatasetId), Verbosity::Normal,
            DataClassification::OrganizationIdentifiableInformation, TelemetryScope::ExtensionPublisher, 'Category', PowerBIServiceMgt.GetPowerBiTelemetryCategory());

        if not OperationResult.Successful then begin
            if OperationResult.ShouldRetry then
                GlobalUploadTracker.ScheduleRetry(GetRetryAfterOrDefault(OperationResult.RetryAfter))
            else
                // If it fails and we should not retry, we should ignore the step (and the dependent steps) and try to go ahead (e.g. for custom uploaded reports)
                GlobalUploadTracker.TransitionTo(Enum::"Power BI Upload Status"::ParametersUpdated);

            Session.LogMessage('0000I21', UpdatingParametersFailedTelemetryMsg, Verbosity::Normal, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::ExtensionPublisher, 'Category', PowerBIServiceMgt.GetPowerBiTelemetryCategory());
            exit;
        end;

        // Update datasource
        Clear(OperationResult);

        PowerBIServiceProvider.UpdateDatasourceCredentials(DataSourceId, GatewayId, BusinessCentralAccessToken, OperationResult);

        if not OperationResult.Successful then begin
            Session.LogMessage('0000I22', UpdatingParametersFailedTelemetryMsg, Verbosity::Normal, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::ExtensionPublisher, 'Category', PowerBIServiceMgt.GetPowerBiTelemetryCategory());

            if OperationResult.ShouldRetry then begin
                GlobalUploadTracker.ScheduleRetry(GetRetryAfterOrDefault(OperationResult.RetryAfter));
                exit;
            end;
        end;

        GlobalUploadTracker.TransitionTo(Enum::"Power BI Upload Status"::ParametersUpdated);
    end;

    local procedure RefreshDataset(PowerBIServiceProvider: Interface "Power BI Service Provider")
    var
        OperationResult: DotNet OperationResult;
        DatasetId: Text;
    begin
        DatasetId := GlobalUploadTracker.GetDatasetId();

        Session.LogMessage('0000G22', StrSubstNo(RefreshingDatasetTelemetryMsg, DatasetId), Verbosity::Normal,
            DataClassification::OrganizationIdentifiableInformation, TelemetryScope::ExtensionPublisher, 'Category', PowerBIServiceMgt.GetPowerBiTelemetryCategory());

        PowerBIServiceProvider.RefreshDataset(DatasetId, OperationResult);

        if OperationResult.Successful then
            GlobalUploadTracker.TransitionTo(Enum::"Power BI Upload Status"::DataRefreshed)
        else
            if OperationResult.ShouldRetry then
                GlobalUploadTracker.ScheduleRetry(GetRetryAfterOrDefault(OperationResult.RetryAfter))
            else
                Error(PowerBIDatasetRefreshFailedErr);
    end;

    local procedure GetRetryAfterOrDefault(RetryAfter: DateTime): DateTime
    begin
        if RetryAfter <> 0DT then
            exit(RetryAfter);
        exit(CurrentDateTime());
    end;

    local procedure MakeReportNameForUpload(PbixReportName: Text; EnvironmentName: Text; CompanyNameIn: Text): Text
    begin
        exit(StrSubstNo(ReportEnvNameTxt, PbixReportName, EnvironmentName, CompanyNameIn));
    end;

}