M365,  Power Apps

Quick Takeaway From Using Microsoft Graph Toolkit in PCF

The holiday season is the best time to dust off quirky items from your backlog and play with it. Frankly, this one has been sitting in my to-do for quite some time, I have always wanted to see how Microsoft Graph Toolkit would work within the PCF context. Here is a quick post to share some of the findings and lessons learned.

Here is a breakdown of items we are going to cover in this post:

  • Short introduction of Microsoft Graph Toolkit
  • Set up a PCF control with Microsoft Graph Toolkit
  • Discuss some of the implementation challenges – battle of trials and errors

 

What is Microsoft Graph Toolkit (MGT)?

The official explanation from Microsoft Docs:

The Microsoft Graph Toolkit is a collection of reusable, framework-agnostic components and authentication providers for accessing and working with Microsoft Graph.

Reference – Toolkit Overview

In a nutshell, Microsoft has provided a collection of web components that implements the most commonly built experiences powered by Microsoft Graph API, such as displaying a group of people, tasks or agendas. There is also the various implementations of providers that encapsulate the authentication process and the acquiring of access tokens with which to call the Microsoft Graph API.

One of the big advantages of using the toolkit is that these are framework-agnostic web components. Which means it can be consumed from any web-based application, even as PCF controls on a model-driven Power App! The fact that it already has a Login web component and standalone Msal provider is music to my ears 😊. Not only that, most of my PCF controls are written in React and there is now an @microsoft/mgt-react package which wraps Microsoft Graph Toolkit web components into React components. All with type declarations in TypeScript.

As you can see, benefits so far are tremendous:

  • Reusable components
  • Works in all standard web apps in modern browsers
  • Beautiful aesthetics that fit consistently with other Microsoft 365 experiences

If you want to learn more about the Microsoft Graph Toolkit then visit the Microsoft Docs and also explore the working demos at the Microsoft Graph Toolkit playground.

 

Quick PCF Control Setup

First, create your PCF control project using the PCF CLI command pac pcf init. We need to install the following npm packages:

npm install react react-dom @microsoft/mgt-element @microsoft/mgt-msal-provider @microsoft/mgt-react
Note: With general availability of Microsoft Graph Toolkit 2.0, what used to be a single package containing all components and providers has been split into multiple packages. Import @microsoft/mgt to include all as one single package.   

 

Create Azure AD App Registration

Before we can call Microsoft Graph API, we first need to create an Azure AD App registration. Please follow the steps outlined here. Make sure to include your Power Apps environment URL as the redirect URI:

💡 Something different I've noticed here is that I had to specify the full path which includes the /main.aspx. From my previous experience working with the MSAL library, we only had to specify the domain url. 

 

Configure the API Permissions

Under the newly created Azure AD App Registration, navigate to Manage > API permission. Add new permissions by selecting Microsoft Graph and Delegated permissions:

Select the following permissions and click Grant admin consent:

This list of permissions may seem numerous but the reason for it will be explained in the implementation section.

 

Create and Configure MSAL Provider

From the PCF control, within the index.ts, the MSAL provider is simply instantiated by the highlighted lines of code below:

public async updateView(context: ComponentFramework.Context<IInputs>): Promise<void> {
	if (this._providerInitialised || (this._providerInitialised = await this.initProvider(context))) {

		const appProps: IAppProps = {
			componentType: context.parameters.componentType.raw!,
			azureADGroupId: context.parameters.azureADGroupId.raw ?? undefined
		};

		ReactDOM.render(
			React.createElement(App, appProps),
			this._container
		);
	}
}

private async initProvider(context: ComponentFramework.Context<IInputs>): Promise<boolean> {
	const azureADClientId = 
		await this._environmentHelper.getValue(context.parameters.env_azureADClientId.raw);
	if (azureADClientId == null) return false;

	Providers.globalProvider = new MsalProvider({
		clientId: azureADClientId,
		scopes: ['openid', 'profile', 'user.read', 'user.readbasic.all', 'user.read.all',
			'calendars.read', 'group.read.all', 'groupMember.read.all',
			'people.read', 'people.read.all', 'presence.read', 'presence.read.all',
			'mail.readbasic', 'sites.read.all'],
		loginType: LoginType.Popup,
		options: {
			auth: {
				clientId: azureADClientId,
				postLogoutRedirectUri: window.location.href
			}
		}
	});

	return true;
}

It took me several attempts to get the correct settings to work nicely in the Power Apps context. Please note the following:

  • loginType – make sure to have the value set to LoginType.Popup. If you use LoginType.Redirect, the authentication screen will take over your current browser tab and, once logged in, it will redirect you to the homepage and not the form page you were previously on. I tried using the navigateToLoginRequestUrl option but it still doesn’t redirect correctly.
  • options – this is necessary to specify the post-logout redirect URI. Without this, it will redirect to the homepage when users log out. A little annoyance but the client id must be redefined as it’s a mandatory property.
  • scopes – all the permission scopes required to be granted to the control to access the data.

 

Implementation Challenges – Battle of Trials and Errors
Using icons to annotate - ⚠ Issue, 🔨 Workaround, 👽 Unproven, ❌ Only for play not for real-world 

Challenge #1 – Only one PCF control with Microsoft Graph Toolkit npm imports allowed in a single form

⚠ How I envisioned Microsoft Graph Toolkit to work with PCF was dashed when I realised that there can only be one PCF control configured with the Microsoft Graph Toolkit defined in one form. For example, the scenario where you have 3 different PCF controls A, B and C all importing and instantiating the provider and/or components on the same form will result in an error. This is somewhat related to this open issue SharePoint developers are experiencing when hosting multiple MGT web-parts on the same page.

🔨 What I’ve realised is that one PCF control configured multiple times on the same form works. So instead of writing multiple PCF controls to modulate the different Microsoft Graph functionalities, I have written one monolithic PCF control with an enum property to specify the sub-component I want to render.

From the ControlManifest.Input.xml (Note: using … to make it more readable):

<control namespace="TRH" constructor="MgtControl" version="0.0.1" ... control-type="standard">
  ...
  <property name="bound" ... of-type="SingleLine.Text" usage="bound" required="true" />
  <property name="azureADGroupId" ... of-type="SingleLine.Text" usage="bound" required="false" />
  <property name="componentType" ... of-type="Enum" usage="input" required="true" default-value="login">
    <value name="login" ...>login</value>
    <value name="agenda" ...>agenda</value>
    <value name="user" ...>user</value>
    <value name="people" ...>people</value>
  </property>
  <property name="env_azureADClientId" ... of-type="SingleLine.Text" usage="input" required="true" />
  ...
</control>

Currently this PCF control has 4 sub-component type options:

  • Login
  • User (currently logged-in user)
  • People (users under a specified Azure AD group id)
  • Agenda (events in a group calendar)

In the App.tsx:

import React = require("react");
import { useState, useEffect } from "react";
import { Providers, ProviderState } from "@microsoft/mgt-element";
import { LoginFC } from "./Login";
import { PeopleFC } from "./People";
import { UserFC } from "./User";
import { AgendaFC } from "./Agenda";

export interface IAppProps {
    componentType: "login" | "agenda" | "user" | "people",
    azureADGroupId?: string
}

export const App: React.FC<IAppProps> = (props: IAppProps) => {
    const [isSignedIn] = useIsSignedIn();
    const { azureADGroupId, componentType } = props;

    let component = null;
    switch (componentType) {
        case "login":
            component = <LoginFC />
            break;
        case "agenda":
            component = <AgendaFC isSignedIn={isSignedIn} azureADGroupId={azureADGroupId} />
            break;
        case "user":
            component = <UserFC isSignedIn={isSignedIn} />
            break;
        case "people":
            component = <PeopleFC isSignedIn={isSignedIn} azureADGroupId={azureADGroupId} />
            break;
    }

    return (<div className="app">
        {component}
    </div>);
};

const useIsSignedIn = (): [boolean] => {
    const [isSignedIn, setIsSignedIn] = useState(false);
    useEffect(() => {
        const updateState = () => {
            const provider = Providers.globalProvider;
            setIsSignedIn(provider && provider.state === ProviderState.SignedIn);
        };

        Providers.onProviderUpdated(updateState);
        updateState();

        return () => {
            Providers.removeProviderUpdatedListener(updateState);
        };
    }, []);
    return [isSignedIn];
}

 

Challenge #2 – Same list of permission scopes for each PCF control instance

⚠ Setting an instance of MsalProvider as one singleton Providers.globalProvider is neat if you have a SPA. This is by design, to allow authentication logic and granting of permissions in one place for the entire app. However, due to the self-containing nature of PCF controls, having authentication and authorisation done once might not be ideal. As the default MSAL behaviour, details such as access tokens are cached in the local browser storage, therefore it will get shared by all MGT PCF controls authorized with the same authority and client id. Even if a control using the same client id wants to limit the permission scopes it won’t be able to completely isolate itself.

🔨 Since there is no way to avoid this, I’ve decided to list (hard code) all the permission scopes within the PCF control itself. It lists all the least privileged scopes required to render the different sub-components made available.

 

Challenge #3 – Flyout controls don’t position correctly in Power Apps

⚠ The nice flyout controls implemented with some of the Microsoft Graph Toolkit web components don’t position correctly when made visible. For example, the Login control displays the flyout control to the middle of the screen, like this:

If positioned correctly it should look like the following:

The issue is due to the way the Power Apps page is composed and styled. It has multiple nested elements which impose a new stacking context to be created using the CSS property – transform: rotate(0deg). Any child element with position: fixed or absolute under this new stacking context will calculate its position according to this new containing block. This is why the calculation for the flyout position is incorrect.

👽 There is only one solution for this which is to extend the component to gain more control over how the flyout controls are rendered. Even doing so may be difficult with respect to UX and confinement of PCF (i.e. what it can and cannot access outside its boundary).

❌ There is a quick workaround for “playing” purpose to make flyout controls position correctly (PLEASE DO NOT IMPLEMENT THIS FOR PRODUCTION). You can insert this hack in the init method of the PCF control:

const elems = this._container.ownerDocument.getElementById("mainContent").getElementsByClassName("forceNewStackContext");
for (let i = 0; i < elems.length; i++) {
	elems[i].setAttribute("style", "transform: unset");
}

 

Here is a demo of what it looks like in action:

In conclusion, even though I would love to find cool use-cases to utilise Microsoft Graph Toolkit in the context of PCF controls, the issues I have run into make it difficult to do so at the moment. Hopefully in the near future things may be different.

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