2/11/2022 Admin

Blazor Org Chart Manager Using The Microsoft Graph


Using the Syncfusion Blazor Diagram component, we can display an Organizational Chart, using Microsoft Blazor, from data contained in the Microsoft Graph. In addition, we can update the manager of the users, by dragging and dropping the boxes representing them.

image

If we configure and run the sample code (available on the Downloads page if this site), we can see all the users, in the group, and their manager in the Microsoft Graph.

image

The Home page will display an Organizational Chart using the Syncfusion Blazor Diagram component, using the data in the Microsoft Graph.

image

We can also select a note on the Org Chart and drag it on top of another node to set the Manager in the Microsoft Graph.

Azure Set-Up

image

If you are an Administrator in your Microsoft Azure Tenant, you can also go to https://portal.azure.com/ and select the Azure Active Directory node.

image

Select Groups, then New Group.

image

Create a Security group called Organizational Chart.

image

Select the group and add members to the group.

image

Now you need to ensure each member, that has a Manger, has a Manger assigned

To do this, you can select a user, select their Profile, and select Edit to edit a user…

image

...and set their Manager in the Microsoft Graph.

You can also set their Job title.

image

Note: If you have a subscription to Microsoft 365, and are the Administrator, you can also go to the Microsoft 365 admin center (https://admin.microsoft.com/), select a user, and set their Manager.

image

Next, follow this link to find the directions to Register a Microsoft Graph application.

image

Ensure you add these permissions to the Azure Application Registration you create (and you click the Grant admin consent button after adding them):

  • Directory.Read.All
  • User.Read
  • User.ReadWrite.All

The Code

Note: The sample code implements Syncfusion Blazor components. A license notification will show unless you obtain a license from: https://www.syncfusion.com/downloads/blazor

image

In the appsettings.json file of the sample application, fill in the settings from your Azure tenant and Azure Application Registration, in the AzureAd section.

image

The EmployeeGraphData.cs file contains the method that provides the data for both the Org Chart and the All Users page.

This is the code for the method:

								
									public
									async Task<List<OrganizationalDetails>> GetEmployeesAsync()
								
									{
								
									// Get Group Id
								
									// This requires Directory.Read.All "Application" Permission
								
									string[] scopes =
									new
									string[] { "https://graph.microsoft.com/.default" };
								

								
									var options =
									new
									TokenCredentialOptions
								
									{
								
									AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
								
									};
								

								
									// https://docs.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
								
									var clientSecretCredential =
									new
									ClientSecretCredential(
								
									_configuration["AzureAd:Tenant"],
								
									_configuration["AzureAd:ClientId"],
								
									_configuration["AzureAd:ClientSecret"],
								
									options);
								

								
									var graphClient =
									new
									GraphServiceClient(clientSecretCredential, scopes);
								

								
									string
									OrganizationalChartGroup = "Organizational Chart";
								

								
									var GroupCollection = await graphClient.Groups.Request()
								
									.Filter($"startsWith(displayName,'{OrganizationalChartGroup}')")
								
									.GetAsync();
								

								
									var GroupId = GroupCollection
								
									.ToList()
								
									.Where(g => g.DisplayName == OrganizationalChartGroup)
								
									.FirstOrDefault().Id;
								

								
									// Get users and their Managers
								
									// This requires User.Read.All "Application" Permission
								
									IConfidentialClientApplication confidentialClientApplication =
								
									ConfidentialClientApplicationBuilder
								
									.Create(_configuration["AzureAd:ClientId"])
								
									.WithTenantId(_configuration["AzureAd:Tenant"])
								
									.WithClientSecret(_configuration["AzureAd:ClientSecret"])
								
									.Build();
								

								
									AuthenticationResult result =
									null;
								

								
									result = await confidentialClientApplication.AcquireTokenForClient(scopes)
								
									.ExecuteAsync();
								

								
									var httpClient =
									new
									HttpClient();
								
									var apiCaller =
									new
									ProtectedApiCallHelper(httpClient);
								

								
									var callResult = await apiCaller
								
									.CallWebApiAndProcessResultASync(
								
									"https://graph.microsoft.com/v1.0/groups/"
								
									+ GroupId +
								
									"/members/microsoft.graph.user?$expand=manager($levels=max)",
								
									result.AccessToken
								
									);
								

								
									List<GraphUser> colGraphUsers =
									new
									List<GraphUser>();
								

								
									foreach
									(JProperty child
									in
									callResult.Properties()
								
									.Where(p => !p.Name.StartsWith("@")))
								
									{
								
									colGraphUsers.AddRange(
								
									child.Value.ToObject<List<GraphUser>>()
								
									);
								
									}
								

								
									List<OrganizationalDetails> DataSource =
									new
									List<OrganizationalDetails>();
								

								
									foreach
									(var objUser
									in
									colGraphUsers)
								
									{
								
									DataSource.Add(new
									OrganizationalDetails()
								
									{
								
									Id = objUser.id,
								
									Role = objUser.jobTitle,
								
									DisplayName = objUser.displayName,
								
									Manager = (objUser.manager !=
									null) ? objUser.manager.id : "",
								
									ManagerName = (objUser.manager !=
									null) ? objUser.manager.displayName : ""
								
									});
								
									}
								

								
									// Need to sort so the managers are at the top of the collection
								
									return
									DataSource.OrderBy(x => x.Manager).ToList();
								
									}

image

The Org Chart is contained on the Index.razor page.

The markup is shown below:

								
									<
									div
									id="diagram-space"
									class="content-wrapper"
									>
								
									<
									SfDiagramComponent
									@ref="Diagram"
									Height="690px"
									DragDrop="Drop"
								
									NodeCreating="NodeCreating"
									ConnectorCreating="ConnectorCreating"
									>
								
									<
									DataSourceSettings
									Id="Id"
									ParentId="Manager"
									DataSource="colGraphUsers"
									>
									</
									DataSourceSettings
									>
								
									<
									Layout
									@bind-Type="type"
									@bind-HorizontalSpacing="@HorizontalSpacing"
								
									@bind-FixedNode="@FixedNode"
									@bind-Orientation="@oreintation"
								
									@bind-VerticalSpacing="@VerticalSpacing"
									@bind-HorizontalAlignment="@horizontalAlignment"
								
									@bind-VerticalAlignment="@verticalAlignment"
									GetLayoutInfo="GetLayoutInfo"
									>
								
									<
									LayoutMargin
									@bind-Top="@top"
									@bind-Bottom="@bottom"
								
									@bind-Right="@right"
									@bind-Left="@left"
									>
								
									</
									LayoutMargin
									>
								
									</
									Layout
									>
								
									<
									SnapSettings
									Constraints="SnapConstraints.None"
									>
									</
									SnapSettings
									>
								

								
									</
									SfDiagramComponent
									>
								
									</
									div
									>
								

The data is retrieved in the OnInitializedAsync() method, from the GetEmployeesAsync() method:

								
									protected
									override
									async Task OnInitializedAsync()
								
									{
								
									try
								
									{
								
									colGraphUsers = await _EmployeeGraphData.GetEmployeesAsync();
								
									}
								
									catch
									(Exception ex)
								
									{
								
									strError = ex.GetBaseException().Message;
								
									}
								
									}

The code to process the data and configure the Syncfusion Diagram Component is below:

								
									// Defines default values for Node object
								
									private
									void
									NodeCreating(IDiagramObject obj)
								
									{
								
									Node node = obj
									as
									Node;
								

								
									if
									(node.Data
									is
									System.Text.Json.JsonElement)
								
									{
								
									node.Data = System.Text.Json.JsonSerializer
								
									.Deserialize<OrganizationalDetails>(node.Data.ToString());
								
									}
								

								
									OrganizationalDetails organizationData = node.Data
									as
									OrganizationalDetails;
								

								
									node.Width = 177;
								
									node.Height = 54;
								

								
									node.Constraints = NodeConstraints.Default | NodeConstraints.AllowDrop;
								

								
									node.Annotations =
									new
									DiagramObjectCollection<ShapeAnnotation>()
								
									{
								
									new
									ShapeAnnotation()
								
									{
								
									Content = organizationData.DisplayName + "\r\n" + organizationData.Role,
								
									Style =
									new
									TextStyle(){Color = "black"}
								
									}
								
									};
								
									}
								

								
									private
									void
									ConnectorCreating(IDiagramObject DiagramObject)
								
									{
								
									Connector connector = (DiagramObject
									as
									Connector);
								
									connector.Type = ConnectorSegmentType.Orthogonal;
								
									connector.TargetDecorator.Shape = DecoratorShape.None;
								
									}
								

								
									private
									TreeInfo GetLayoutInfo(IDiagramObject obj, TreeInfo options)
								
									{
								
									if
									(rows == 0)
								
									{
								
									if
									(rows == 0 && options.Rows !=
									null)
								
									options.Rows =
									null;
								
									Node node = obj
									as
									Node;
								

								
									if
									(!options.HasSubTree)
								
									{
								
									options.Orientation = subTreeOrientation;
								
									options.AlignmentType = subTreeAlignment;
								
									}
								
									}
								
									else
								
									{
								
									if
									(!options.HasSubTree)
								
									{
								
									options.AlignmentType = SubTreeAlignmentType.Balanced;
								
									options.Orientation = Orientation.Horizontal;
								
									options.Rows = rows;
								
									}
								
									}
								
									return
									options;
								
									}

If a node is dropped on another node the following method is called:

								
									private
									async Task Drop(DropEventArgs args)
								
									{
								
									DiagramSelectionSettings selector = args.Element
									as
									DiagramSelectionSettings;
								

								
									if
									(args.Target !=
									null
									&& args.Element !=
									null
									&& selector.Nodes.Count > 0)
								
									{
								
									Node targetNode = args.Target
									as
									Node;
								

								
									OrganizationalDetails targetNodeData =
								
									colGraphUsers.Find(data => data.Id == (targetNode.Data
									as
									OrganizationalDetails).Id);
								

								
									OrganizationalDetails dragNodeData =
								
									colGraphUsers.Find(data => data.Id == (selector.Nodes[0].Data
									as
									OrganizationalDetails).Id);
								

								
									// Update the Microsoft Graph
								
									await _EmployeeGraphData.AssignManagerAsync(dragNodeData.Id, targetNodeData.Id);
								

								
									dragNodeData.Manager = targetNodeData.Id;
								
									Diagram.ClearSelection();
								
									await Diagram.RefreshDataSource();
								
									}
								
									}

That code calls the following method that actually updates the Microsoft Graph:

								
									public
									async Task AssignManagerAsync(string
									EmployeeId,
									string
									ManagerId)
								
									{
								
									// This requires User.ReadWrite.All "Application" Permission
								
									// See: https://docs.microsoft.com/en-us/graph/api/user-post-manager?view=graph-rest-1.0&tabs=http
								

								
									string[] scopes =
									new
									string[] { "https://graph.microsoft.com/.default" };
								

								
									var options =
									new
									TokenCredentialOptions
								
									{
								
									AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
								
									};
								

								
									// https://docs.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
								
									var clientSecretCredential =
									new
									ClientSecretCredential(
								
									_configuration["AzureAd:Tenant"],
								
									_configuration["AzureAd:ClientId"],
								
									_configuration["AzureAd:ClientSecret"],
								
									options);
								

								
									var graphClient =
									new
									GraphServiceClient(clientSecretCredential, scopes);
								

								
									await graphClient.Users[EmployeeId].Manager.Reference.Request().PutAsync(ManagerId);
								
									}

Troubleshooting

image

If you get an error when running the sample code…

image

Access the Developer tools to see the error.

image

Clicking on the Console tab, and selecting the first error, will reveal the issue.

If you get the “The given key.. was not present in the dictionary” error, it is usually because you have more than one user who does not have a Manager set (so the diagram does not know where to start).

The solution is to alter the users placed in the group to only include users who have a manager properly assigned.

Download

The project is available on the Downloads page on this site.

You must have Visual Studio 2022 (or higher) installed to run the code.

Links

(Syncfusion) Create an Org Chart to Elegantly Visualize Hierarchical Data in Blazor WebAssembly

(Syncfusion Sample) Example of Remote Data in Blazor Diagram Component

(Syncfusion Sample) Organization Chart Example using Blazor Diagram Component

(Microsoft Graph) https://docs.microsoft.com/en-us/graph/api/user-post-manager?view=graph-rest-1.0&tabs=http
(Microsoft Graph) https://docs.microsoft.com/en-us/graph/api/user-list-manager?view=graph-rest-1.0&tabs=http
(Microsoft Graph) https://docs.microsoft.com/en-us/graph/api/user-list-directreports?view=graph-rest-1.0&tabs=http
(Microsoft Graph) https://docs.microsoft.com/en-us/graph/api/user-list-manager?view=graph-rest-1.0&tabs=csharp%2Chttp#example-2-get-manager-chain-up-to-the-root-level

An error has occurred. This application may no longer respond until reloaded. Reload 🗙