{"id":470,"date":"2023-08-11T12:08:13","date_gmt":"2023-08-11T10:08:13","guid":{"rendered":"https:\/\/www.paalolav.no\/?p=470"},"modified":"2023-10-02T13:23:46","modified_gmt":"2023-10-02T11:23:46","slug":"org-chart-with-a-twist","status":"publish","type":"post","link":"https:\/\/www.paalolav.no\/?p=470","title":{"rendered":"Org chart with a twist"},"content":{"rendered":"\n<p>A customer really liked Microsoft&#8217;s Org chart web part in SharePoint. However, they needed to be able to display data from the manager-field as well as a custom field for line-manager combined in a single view. I also found great joy being able to create a menu for switching between the different departments without leaving the page.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.paalolav.no\/wp-content\/uploads\/2023\/10\/Org-Chart.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"561\" src=\"https:\/\/www.paalolav.no\/wp-content\/uploads\/2023\/10\/Org-Chart-1024x561.png\" alt=\"\" class=\"wp-image-485\" srcset=\"https:\/\/www.paalolav.no\/wp-content\/uploads\/2023\/10\/Org-Chart-1024x561.png 1024w, https:\/\/www.paalolav.no\/wp-content\/uploads\/2023\/10\/Org-Chart-300x164.png 300w, https:\/\/www.paalolav.no\/wp-content\/uploads\/2023\/10\/Org-Chart-768x421.png 768w, https:\/\/www.paalolav.no\/wp-content\/uploads\/2023\/10\/Org-Chart.png 1242w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<p>Their data comes from a couple of sources, the data source for this example comes from an extension attribute in Entra ID\/Azure AD.<\/p>\n\n\n\n<p>In the solution I use Modern PnP Search for the front end and an Automation Runbook in an Azure Subscription for moving data to the SharePoint User profiles.<\/p>\n\n\n\n<p>When the customer needs to change or add a manager, they have to enter the manager&#8217;s e-mail address in the Search Query box, or as the above screen shot demonstrates, use a Vertical connected to the input query text as a menu for each department. This solution also has clickable contacts in the mgt-person hover menu, so that you can deep dive to the resources reporting to the manager you&#8217;ve selected.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Search Query<\/h2>\n\n\n\n<p>Replace with managed properties you want to use.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:0.8rem\"><code>Manager:\"{SearchTerms}\" OR FunctionalManager:\"{SearchTerms}\" -UserName:\"{SearchTerms}\"<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Modern PNP Search template<\/h2>\n\n\n\n<p>I use mgt-person quite a lot to make this happen, with some funky CSS-styling in order to get the right look and feel.<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:0.8rem\"><code>&lt;content id=\"data-content\">\n    &lt;style>\n        .template--header {\n            flex-wrap: wrap;\n            display: flex;\n            justify-content: center;\n        }\n        mgt-person {\n            --initials-background-color: {{@root.theme.palette.themePrimary}};\n        }\n        .Manager {\n            display: block;\n            margin-left: auto;\n            margin-right: auto;\n            padding: 8px;\n            width: 380px;\n            height: 50px;\n            background-color: white;\n            border: solid;\n            border-width: 1px;\n            border-color: {{@root.theme.palette.neutralQuaternary}};\n            border-radius: 5px;\n        }\n        .counter {\n            font-size: 12px;\n            margin-left: 5px;\n            padding: 5px;\n        }\n\n        .reportsto {\n            --avatar-size: 0px;\n            --font-size: 12px;\n            --font-weight: 400;\n            --color:black;\n            margin-left: -5px;\n        }\n\n        .employees {\n            flex-wrap: wrap;\n            display: flex;\n            justify-content: space-between;\n            padding: 5px;\n        }\n        .template--defaultList {\n            padding: 5px;\n            background-color: {{@root.theme.palette.neutralLighterAlt}};\n            border: solid;\n            border-width: 1px;\n            border-color: {{@root.theme.palette.neutralQuaternary}};\n            border-radius: 5px;\n        }\n        .template--peopleListItem {\n            width: 300px;\n        }\n    &lt;\/style>\n\n    &lt;div class=\"template\">\n        &lt;div class=\"Manager\">\n                    &lt;mgt-person person-query=\"{{getUserEmail inputQueryText}}\" fallback-details='{\"displayName\": \"Searching for user.\"}' view=\"twoLines\" line2-property=\"jobTitle\" person-card=\"hover\">\n                    &lt;\/mgt-person-card>&lt;br>\n        &lt;\/div>&lt;\/br>\n            {{#if data.items}}\n        &lt;div class=\"template--header\">\n        &lt;div class=\"template--defaultList\">\n            &lt;div class=\"counter\">\n            People reporting to &lt;mgt-person class=\"reportsto\" person-query=\"{{getUserEmail inputQueryText}}\" view=\"oneLine\">&lt;\/mgt-person> ({{@root.data.totalItemsCount}}) \n            &lt;\/div>\n            {{\/if}}\n            &lt;ul class=\"employees\">\n                {{#each data.items as |item|}}\n                    &lt;pnp-select \n                        data-index=\"{{@index}}\"\n                        &lt;template id=\"content\">\n                                {{#> resultTypes item=item}}\n                            &lt;li class=\"template--peopleListItem\" tabindex=\"0\">\n                                        &lt;mgt-person user-id=\"{{getUserEmail (slot item @root.slots.PersonQuery)}}\" person-card=\"hover\">\n                                            &lt;template>\n                                                &lt;pnp-persona \n                                                    data-image-url=\"\\{{personImage}}\" \n                                                    data-fields-configuration=\"{{JSONstringify @root.properties.layoutProperties.peopleFields}}\" \n                                                    data-item=\"{{JSONstringify item}}\" \n                                                    data-persona-size=\"small\"\n                                                    data-theme-variant=\"{{JSONstringify @root.theme}}\"\n                                                    data-instance-id=\"{{@root.instanceId}}\"\n                                                    data-context=\"{{JSONstringify @root}}\">\n                                                &lt;\/pnp-persona>\n                                            &lt;\/template>\n                                            &lt;template data-type=\"person-card\">\n                                                &lt;mgt-person-card inherit-details>\n                                                    &lt;template data-type=\"additional-details\">\n                                                        &lt;a href=\"https:\/\/tenant.sharepoint.com\/sites\/OurPeople\/SitePages\/View-manager.aspx?m={{getUserEmail UserName}}\">&lt;h3>Direct reports&lt;\/h3>\n                                                    &lt;\/a>\n                                                    &lt;\/template>\n                                                &lt;\/mgt-person-card>\n                                            &lt;\/template>\n                                        &lt;\/mgt-person>\n                                    \n                                {{\/resultTypes}}\n                            &lt;\/li>\n                        &lt;\/template>\n                    &lt;\/pnp-select>\n                {{\/each}}\n            &lt;\/ul>\n        &lt;\/div>\n    &lt;\/div>\n&lt;\/content><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Azure Automation<\/h2>\n\n\n\n<p>The automation runs in 7.2 with the Graph Authentication 2 and Graph Users 2 (important), so that we can autenticate with a managed identity, which is absolutely great!<\/p>\n\n\n\n<p>What we basically do is moving all user data in the extension attribute over to SharePoint User profile field <em>SPS-dotted-line<\/em>, which is a people picker field.<br>The properties are stored in a temp file on a site called IntranetHelper, which should be restricted to admins.<\/p>\n\n\n\n<p>Remember to give the automation the proper credentials. (<em>Sites.FullControl.All<\/em> is probably a bit much, but the Automation account in this customer story also does a bit of lifting where these privileges are needed.)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Add-PnPAzureADServicePrincipalAppRole -Principal \u201cGUID-GUID-GUID-GUID\u201c-AppRole \"Directory.Read.All\" -BuiltInType MicrosoftGraph\n\nAdd-PnPAzureADServicePrincipalAppRole -Principal \u201cGUID-GUID-GUID-GUID\u201c-AppRole \"Sites.FullControl.All\" -BuiltInType SharePointOnline\n\nAdd-PnPAzureADServicePrincipalAppRole -Principal \u201cGUID-GUID-GUID-GUID\u201c -AppRole \u201cUser.Read.All\u201d -BuiltInType SharePointOnline<\/code><\/pre>\n\n\n\n<p>Over to the <strong>automation<\/strong>, which also has a dryrun-parameter where you can test with a single user (replace name in code).<\/p>\n\n\n\n<pre class=\"wp-block-code\" style=\"font-size:0.8rem\"><code>&#91;CmdletBinding()]\nparam (\n    &#91;Parameter()]\n    &#91;string]\n    $TenantUrl = \"https:\/\/TENANTNAME.sharepoint.com\",\n    &#91;Parameter()]\n    &#91;string]\n    $ServerRelativePathToStoreUserPropertiesFile = \"\/sites\/IntranetHelper\",\n    &#91;Parameter(Mandatory = $false)]\n    &#91;bool]$DryRun\n)\n\n$ErrorActionPreference = \"Stop\"\n\nImport-Module -Name Microsoft.Graph.Authentication -MinimumVersion 2.0.0\nImport-Module -Name Microsoft.Graph.Users -MinimumVersion 2.0.0\n\n$Url = &#91;System.Uri]$TenantUrl\n$TenantAdminUrl = \"https:\/\/\" + $Url.Authority.Replace(\".sharepoint.com\", \"-admin.sharepoint.com\")\n\nWrite-Output \"Connecting to Microsoft Graph\"\nConnect-MgGraph -Identity\n\nWrite-Output \"Retrieving all users from Microsoft Graph\"\nif ($DryRun) {\n    $RelevantUserProps = Get-MgUser -Property Id,DisplayName,Mail,UserPrincipalName,UserType,OnPremisesExtensionAttributes -Filter \"startsWith(DisplayName, 'REPLACE WITH USER NAME TO TEST WITH')\" | Where-Object {$null -ne $_.OnPremisesExtensionAttributes.ExtensionAttribute7} | ForEach-Object {\n        @{\"UserPrincipalName\"=$_.UserPrincipalName;\"ExtensionAttribute7\"=$_.OnPremisesExtensionAttributes.ExtensionAttribute7}\n    }\n} else {\n    $RelevantUserProps = Get-MgUser -Property Id,DisplayName,Mail,UserPrincipalName,UserType,OnPremisesExtensionAttributes -All | Where-Object {$null -ne $_.OnPremisesExtensionAttributes.ExtensionAttribute7} | ForEach-Object {\n        @{\"UserPrincipalName\"=$_.UserPrincipalName;\"ExtensionAttribute7\"=$_.OnPremisesExtensionAttributes.ExtensionAttribute7}\n    }\n}\n\nWrite-Output \"Converting all relevant properties to JSON\"\n$InputObject = @{\"value\"=$RelevantUserProps}\n($InputObject | ConvertTo-Json) | Out-File \".\\properties.json\" -Encoding utf8 -Force -NoNewline\n\nConnect-PnPOnline -Url $($TenantUrl + $ServerRelativePathToStoreUserPropertiesFile) -ManagedIdentity\n\n$UploadedFile = Add-PnPFile -Path \".\\properties.json\" -Folder \"Shared documents\"\n$UrlToFile = $TenantUrl + $UploadedFile.ServerRelativeUrl\nWrite-Output \"JSON-file generated and uploaded to $UrlToFile\"\n\nConnect-PnPOnline -Url $TenantAdminUrl -ManagedIdentity\n\nWrite-Output \"Scheduling UPA Bulk Import Job\"\nif (-not $DryRun) {\n    $ImportJob = New-PnPUPABulkImportJob -IdProperty \"UserPrincipalName\" -IdType PrincipalName -UserProfilePropertyMapping @{\"ExtensionAttribute7\"=\"SPS-Dotted-line\"} -Url $UrlToFile\n    \n    Write-Output \"Bulk user profile import job scheduled with ID \" $ImportJob.JobId\n    Write-Output \"You can check status of the import by running 'Get-PnPUPABulkImportStatus -JobId $($ImportJob.JobId)'\"\n} else {\n    Write-Output \"Skipping Bulk import because -DryRun was specified\"\n}\nWrite-Output \"Bulk import scheduled and script completed\"<\/code><\/pre>\n\n\n\n<p>Remember to map both the crawled properties for <strong>Manager<\/strong> and <strong>SPS-Dotted-line<\/strong> from the user profile service to respective managed properties and reindex the user profiles.<\/p>\n\n\n\n<p>That&#8217;s basically it. Good luck with your solutions!<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A customer really liked Microsoft&#8217;s Org chart web part in SharePoint. However, they needed to be able to display data from the manager-field as well as a custom field for line-manager combined in a single view. I also found great joy being able to create a menu for switching between the different departments without leaving [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":505,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[86,77,67,76],"tags":[],"class_list":["post-470","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-automations","category-microsoft","category-sharepoint","category-search"],"_links":{"self":[{"href":"https:\/\/www.paalolav.no\/index.php?rest_route=\/wp\/v2\/posts\/470"}],"collection":[{"href":"https:\/\/www.paalolav.no\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.paalolav.no\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.paalolav.no\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.paalolav.no\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=470"}],"version-history":[{"count":10,"href":"https:\/\/www.paalolav.no\/index.php?rest_route=\/wp\/v2\/posts\/470\/revisions"}],"predecessor-version":[{"id":490,"href":"https:\/\/www.paalolav.no\/index.php?rest_route=\/wp\/v2\/posts\/470\/revisions\/490"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.paalolav.no\/index.php?rest_route=\/wp\/v2\/media\/505"}],"wp:attachment":[{"href":"https:\/\/www.paalolav.no\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=470"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.paalolav.no\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=470"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.paalolav.no\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=470"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}