Power Apps

Better PCF ALM with Environment Variables

In my previous posts, I’ve implemented PCF controls that need several property values to be configured when the controls are customised on the entity forms. The flexibility to customise and configure values for these controls are great but this does not scale well if these values need to be different in downstream environments, such as UAT and Production. Hence, this is a quick post to implement a better application lifecycle management (ALM) for PCF controls by utilising the Power Apps environment variables.

Here is what we are going to cover in this post:

  • Discuss the benefit of using environment variables with PCF controls
  • Configure the React AAD MSAL PCF control, from the previous post, with environment variables as control properties
  • Refactor the code to retrieve values from the environment variables

 

Why Use Environment Variables With PCF Controls?

Last year Microsoft released environment variables as a preview feature. In a nutshell, this allows an out-of-the-box way of creating and managing configuration settings for a Power Apps instance. The best part about this is that the definition of the configuration settings and the values are considered as solution components, therefore can be exported and imported as part of a solution. You can read more about this feature here.

So how would this benefit us when developing PCF controls? Most PCF controls shouldn’t require using environment variables as they should be self-containing and any input property values can be hard-coded. However, it would benefit PCF controls that have external dependencies such as implementing web API calls to external services that require authentication or a key or in any scenario where input property values vary depending on the deployed environment.

To give a simple example, you want to call Google Maps API from the PCF control. This requires passing in an API key to consume the service. In best practice, different API keys should be used for different environment tiers such as development and production environments. Let’s say we’ve customised the control and hard-coded the key to the input property. When we export this customisation to import into another environment, we would need to manually customise the control again. We all know that this is a bad idea! This is where using the environment variables would help us. The environment variables work in a key-value pair and while the key would never change across environments, the value can. Instead of configuring the actual value of the API key with the control, we could instead configure the environment variable key and retrieve the value at runtime from the control itself. There is no better way to explain this than to walk through some code to demonstrate!

 

Configure PCF Control Properties With Environment Variables

Let’s customise the control properties I’ve written for the React AAD MSAL PCF control to use the environment variable names instead:

Please refer back to the previous post to see the control properties as they were before using the environment variables.

When working with the environment variables, we will need to retrieve records from the following two entities:

  • Environment Variable Definition – defines properties like the schema name, data type and default value
  • Environment Variable Value – value for the current environment. If none created, then it will use the default value specified in the definition

There are currently four data types available when creating the environment variables (Decimal number, JSON, Text and Two options) and I wanted to demonstrate all four types with this control:

In my development environment I have configured the following three environment variable values:

 

Let’s Refactor the Code!

With the properties set up, now we can move on to consuming it from the code.

In the index.ts, instead of grabbing the values directly from the context parameters, we will need to use the WebApi to retrieve the values and its names from the Environment Variable Value and Environment Variable Definition entity records.

I have decided to make a helper class, EnvironmentHelper, to encapsulate the main procedures for retrieving the environment variable values. This way we can keep the code neat and re-usable.

In index.ts:

import { IInputs, IOutputs } from "./generated/ManifestTypes";
import React = require("react");
import ReactDOM = require("react-dom");
import { initializeIcons } from '@uifabric/icons';
import { Text } from "@fluentui/react/lib/Text"
import { App, IAppProps } from "./App";
import { IConfig } from "./interfaces/IConfig";
import { createMsalAuthProvider } from "./MsalAuthProvider";
import { EnvironmentHelper } from "./environment/EnvironmentHelper";
import { IAzureADSettings } from "./interfaces/IAzureADSetting";

initializeIcons();

export class ReactAadControl implements ComponentFramework.StandardControl<IInputs, IOutputs> {

	private _context: ComponentFramework.Context<IInputs>;
	private _container: HTMLDivElement;
	private _props: IAppProps;
	private _propsLoaded: boolean;

	constructor() { }

	public init(context: ComponentFramework.Context<IInputs>,
				notifyOutputChanged: () => void, 
				state: ComponentFramework.Dictionary, 
				container: HTMLDivElement) {
		this._context = context;
		this._container = container;
		this._propsLoaded = false;
	}

	public async updateView(context: ComponentFramework.Context<IInputs>): Promise<void> {
		if (!this._propsLoaded) {
			try {
				await this.getAppProps();
			} catch (err) {
				console.log("Error has occurred loading initAppProps", err);
			} finally {
				this._propsLoaded = true;
			}
		}

		ReactDOM.render(
			this._propsLoaded && this._props
				? React.createElement(App, this._props)
				: React.createElement(Text, {}, 
					["Please check the context parameter values for this control"]),
			this._container
		);
	}

	public getOutputs(): IOutputs {
		return {};
	}

	public destroy(): void {
		ReactDOM.unmountComponentAtNode(this._container);
	}

	private async getAppProps(): Promise<void> {
		const environmentHelper = new EnvironmentHelper(this._context.webAPI);
		const apiUrl
			= await environmentHelper.getValue(this._context.parameters.env_apiUrl.raw);
		
		const azureADAppSettings: IAzureADSettings 
			= await environmentHelper.getValue(this._context.parameters.env_azureADAppSettings.raw);
		
		const cacheLocation: number 
			= await environmentHelper.getValue(this._context.parameters.env_cacheLocation.raw) || 1;
		
		const forceLogin: boolean 
			= await environmentHelper.getValue(this._context.parameters.env_forceLogin.raw) || false;

		if (apiUrl == null || azureADAppSettings == null) {
			return;
		}

		this._props = {
			msalAuthProvider: createMsalAuthProvider({
				appId: azureADAppSettings.AppId,
				appRedirectUrl: azureADAppSettings.AppRedirectUrl,
				appAuthority: azureADAppSettings.AppAuthority,
				appScopes: azureADAppSettings.AppScopes,
				cacheLocation: cacheLocation == 0 ? "localStorage" : "sessionStorage"
			} as IConfig),
			forceLogin: forceLogin,
			apiUrl: apiUrl
		};
	}
}

I have made a couple of strategic decisions in index.ts:

  • Load the property values only once from the updateView by setting the value of the global boolean variable, _propsLoaded.
  • Make the updateView function an async operation. This waits for the property values to be retrieved and it will handle the rendering of the control depending on the success of the props retrieval.
  • The getAppProps function initialises the props value in one place. This is where all the interesting stuff happens.

In EnvironmentHelper.ts:

import { IEnvironmentVariable, EnvironmentVariableType } from "./IEnvironmentVariable";

export class EnvironmentHelper {
    constructor(private _webAPI: ComponentFramework.WebApi) { }

    async getValue(schemaName: string | null): Promise<any> {
        if (!schemaName) return null;
        const environmentVar = await this.getEnvironmentVariable(schemaName);
        return this.getEnvironmentVariableValue(environmentVar);
    }

    async getEnvironmentVariable(schemaName: string): Promise<IEnvironmentVariable | undefined> {
        const relationshipName = "environmentvariabledefinition_environmentvariablevalue";
        let options = "?";
        options += "$select=schemaname,defaultvalue,type";
        options += `&$filter=statecode eq 0 and schemaname eq '${schemaName}'`;
        options += `&$expand=${relationshipName}($filter=statecode eq 0;$select=value)`;
        const response 
            = await this._webAPI.retrieveMultipleRecords("environmentvariabledefinition", options);

        const environmentVarEntity = response.entities.shift();
        if (environmentVarEntity) {
            const environmentVarType 
                = this.getEnvironmentVariableType(environmentVarEntity.type);
            
            const environmentVarValueEntity 
                = (<any[]>environmentVarEntity[relationshipName]).shift();
            
            const environmentVarValue 
                = environmentVarValueEntity?.value ?? environmentVarEntity.defaultvalue;

            return {
                type: environmentVarType,
                value: environmentVarValue
            };
        }
    }

    getEnvironmentVariableValue(environmentVar?: IEnvironmentVariable): any {
        if (!environmentVar || !environmentVar.value) {
            return null;
        }
    
        let value: any = null;
        switch(environmentVar.type)
        {
            case "string":
                value = this.getStringValue(environmentVar.value);
                break;

            case "boolean":
                value = this.getBooleanValue(environmentVar.value);
                break;

            case "number":
                value = this.getNumberValue(environmentVar.value);
                break;
                
            case "json": 
                value = this.getJsonValue(environmentVar.value);
                break;
        }

        return value;
    }
    
    private getEnvironmentVariableType(typeValue: number): EnvironmentVariableType {
        let type: EnvironmentVariableType;
        switch (typeValue) {
            case 100000000: type = "string"; break;
            case 100000001: type = "number"; break;
            case 100000002: type = "boolean"; break;
            case 100000003: type = "json"; break;
            default: type = "unspecified"
        }
        return type;
    }

    private getStringValue(rawValue: any): string {
        return rawValue as string;
    }

    private getBooleanValue(rawValue: any): boolean {
        return rawValue == "yes" ? true : false;
    }

    private getNumberValue(rawValue: any): number | null {
        const parsedValue = parseFloat(rawValue);
        if (isNaN(parsedValue)) {
            console.log("Error parsing number value", rawValue);
            return null;            
        }
        return parsedValue;
    }

    private getJsonValue(rawValue: any): any {
        try {
            return JSON.parse(rawValue);
        } catch (err) {
            console.log("Error parsing json value", rawValue);
            return null;
        }
    }
}

In IEnvironmentVariable.ts:

export interface IEnvironmentVariable {
    type: EnvironmentVariableType;
    value?: any;
}

export type EnvironmentVariableType = 
    | "string" 
    | "number" 
    | "boolean" 
    | "json" 
    | "unspecified";

We have these notable functions:

  • getValue – the function that acts as a wrapper to return the value of the specified environment variable name to the calling function.
  • getEnvironmentVariable – makes the WebApi call to retrieve the records from CDS (Dataflex Pro). It returns an instance of IEnvironmentVariable that holds the parsed environment variable type and raw value from CDS (Dataflex Pro).
  • getEnvironmentVariableType – raw data type values are just numerical enum values, hence needs to be mapped to recognisable types
  • getEnvironmentVariableValue – parses the value to its correct TypeScript data type according to the corresponding environment variable data type definition value. Interesting ones to look out for are, boolean and JSON data type.

For the dev_SecureAPIAzureAppRegistrationSettings JSON environment variable, I have the following JSON value:

{
   "AppId":"<copied guid of Client ID>",
   "AppAuthority":"https://login.microsoftonline.com/<copied guid of Tenant Id>",
   "AppRedirectUrl":"https://<your-powerapps-environment>.crm6.dynamics.com",
   "AppScopes":[
      "https://<your-api>.azurewebsites.net/user_impersonate"
   ]
}

This can be mapped to an IAzureADSettings interface:

export interface IAzureADSettings {
    AppId: string;
    AppAuthority: string;
    AppRedirectUrl: string;
    AppScopes: string[];
}

OK, that’s it. No more hard-coded values customised on the entity form! I might cover how to DevOps this part in the future when the environment variable comes out of preview feature (but who knows… I might do it sooner!)

Hope you enjoyed the post, let me know your thoughts in the comments below or any of my contact channels 😊

7 Comments

  • Danyil

    Gorgeous article. Perhaps you can improve performance by caching EnvironmentVariable parameters in the session after they have been loaded.

    Great job Tae.

  • Danyil

    By the way, I liked your previous article “Using React AAD MSAL to call a secure API from a PCF Control” and I would like to comment on it, but it looks like the comments are disabled.

    • taerim.han

      This is a bit strange, I’ve got comments enabled for the other posts but it seems WordPress is not playing along. Until I get this working, you can always DM me on Twitter if you have any suggestions or comments about that post! Thank you!