diff --git a/samples/react-site-provisioning-manager/.gitignore b/samples/react-site-provisioning-manager/.gitignore new file mode 100644 index 000000000..52e23cd6b --- /dev/null +++ b/samples/react-site-provisioning-manager/.gitignore @@ -0,0 +1,2 @@ +.vscode +/webpart/package-lock.json diff --git a/samples/react-site-provisioning-manager/README.md b/samples/react-site-provisioning-manager/README.md new file mode 100644 index 000000000..ce4868166 --- /dev/null +++ b/samples/react-site-provisioning-manager/README.md @@ -0,0 +1,161 @@ +# Site Provisioning Manager Web Part + +## Summary +This sample shows how you can manage site provisioning by calling Azure functions. + +You can also find out how you can use React Hooks to manage the state of your application and share data across all components. + + +![react-provisioning-manager](./assets/screenshot.gif) + +## Used SharePoint Framework Version +![drop](https://img.shields.io/badge/version-1.9-green.svg) + +## Applies to + +* [SharePoint Framework](https://dev.office.com/sharepoint) +* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment) + +## Solution + +Solution|Author(s) +--------|--------- +react-site-provisioning-manager | Ramin Ahmadi + +## Version history + +Version|Date|Comments +-------|----|-------- +1.0|August 14, 2019|Initial release + +## Features +This sample illustrates the following concepts on top of the SharePoint Framework: + +* Using React Hooks. +* Using aadHttpClientFactory to call Azure functions. +* PnP/graph to call Microsoft Graph Api. + +## Configure Azure Function + +### Create a self signed certificate + +1. Run below command using Create-SelfSignedCertificate.ps1 in powershell-scripts folder. + +``` +.\Create-SelfSignedCertificate.ps1 -CommonName "NAME" -StartDate 2019-08-11 -EndDate 2025-08-11 -Password (ConvertTo-SecureString -String "PASSWORD" -AsPlainText -Force) +``` + +> The dates are provided in US date format: YYYY-MM-dd +> Don't forget to update the PASSWORD and NAME. + +### Publishing the Azure function app + +Follow below steps in order to publish the functions: + +1. Open Provisioning App solution with Visual Studio 2017/2019. +2. Copy the .pfx certificate you generated under the Cert folder. +3. Open ProvisioningApp.csproj in a text editor and make sure your cert name is included. If not, replace provisioningapp.pfx with your cert file name. +2. In Solution Explorer, right-click the project and select Publish. +3. In the Pick a publish target dialog, use the publish options as specified in the table below the image: + +![publish-profile](./assets/functions-visual-studio-publish-profile.png) + +4. Select Publish. If you haven't already signed-in to your Azure account from Visual Studio, select Sign-in. +5. In the App Service: Create new dialog, enter the hosting settings. +6. Select Create to create a function app and related resources in Azure with these settings and deploy your function project code. + +## Setting up an Azure AD app for app-only access + +### Create a new app registration in Azure AD + +1. Open Azure Portal https://portal.azure.com. +2. Click on Azure Active Directory. +3. Click on App registrations. +4. Click on New registration. +5. Give youre registration a name. +6. Click Register. + +### Add your certificate to the app registration + +1. Open Azure Portal https://portal.azure.com. +2. Select Azure Active Director, App Registration and then the App your created in previous steps. +3. Click on "Certificates & secrets". +4. Click on the "Upload certificate" button. +5. Select the .CER file you generated earlier and click on "Add" to upload it. + +### API permissions + +1. In the app registration we created earlier, click on API Permissions. +2. Click on the "Add a permission" button. +3. Choose the following permissions: +* SharePoint -> Application permissions -> Sites -> Sites.FullControl.All +4. Click Add permissions to save +5. Click Grant admin consent for the permissions to come into effect. +![API Permissions](./assets/api-permissions.png) + +### Add the user_impersonation scope + +Still in your Azure AD app, do the following: + +1. Click on Expose API. +2. Click on Add scope +3. Approve the suggested URL or change it, if you like. +4. Fill in the following info: +- Scope name: `user_impersonation` +- Admin consent display name: `Access YourAzureAdAppDisplayName` +- Admin consent description: `Allow the application to access YourAzureAdAppDisplayName on behalf of the signed-in user.` +3. Press Add scope to save. + +### Securing the Azure function app + +1. Open Azure Portal https://portal.azure.com. +2. Click App Services and find the app you created earlier. +3. Click "Platform features" tab. +4. Under Networking, click "Authentication / Authorization". +5. In the option “App Service Authentication”, select “ON”. +6. For "Action to take when request is not authenticated" option, select “Log in with Azure Active Directory”. +7. Under “Authentication Providers”, select “Azure Active Directory”. +8. Select “Management mode” as Express. +9. Select the Azure AD app we registered earlier. +10. Click OK and then Save. + +### Enable CORS on Azure Function + +1. Click Platform features. +2. Under API, click CORS. +3. Specify the Office 365 tenant domain url and SharePoint local workbench url. +4. Click Save. + +![CORS Settings](./assets/functions-CORS-settings.PNG) + +### Update App Settings + +1. Go the `App Settings` page of the Azure functions. +2. Create new key/value entries under ‘App settings’ as per the following table: + +Key|Value|Note +---|-----|---- +CERTIFICATE| .pfx file name | you should copy .pfx file in Cert folder +PASSWORD| Password you set for the certificate file +CLIENTID| Application Registration Client ID| you can find the client id from overview tab +TENANT| e.x. contoso.onmicrosoft.com + +## Installing the web part + +In the package-solution.json, replace the value of `resource` (under `webApiPermissionRequests`) with the name of your Azure AD app registration. + +On the command line run (when in `webparts` dir): +- `npm install` +- `gulp bundle --ship` +- `gulp package-solution --ship` +- Drop the .sppkg file under `sharepoint\solution` to your tenant app catalog. +- Approve the API permissions via the new SharePoint admin center. + +## Configuring the web part on a page + +Open the web part configurations and set the values: +1. Application Id/EndPoint: the client ID of the Azure AD app registration used for authentication +2. Get provisioning function URL: Go to the Azure functions in Azure portal and click on "GetProvisioningTemplate" and then "Get function Url". Copy-paste that value in this field. +3. Apply provisioning function URL: Go to the Azure functions in Azure portal and click on "ApplyProvisioningTemplate" and then "Get function Url". Copy-paste that value in this field. + + \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/assets/api-permissions.png b/samples/react-site-provisioning-manager/assets/api-permissions.png new file mode 100644 index 000000000..ebc1101d9 Binary files /dev/null and b/samples/react-site-provisioning-manager/assets/api-permissions.png differ diff --git a/samples/react-site-provisioning-manager/assets/functions-CORS-settings.PNG b/samples/react-site-provisioning-manager/assets/functions-CORS-settings.PNG new file mode 100644 index 000000000..bf90a8fca Binary files /dev/null and b/samples/react-site-provisioning-manager/assets/functions-CORS-settings.PNG differ diff --git a/samples/react-site-provisioning-manager/assets/functions-visual-studio-publish-profile.png b/samples/react-site-provisioning-manager/assets/functions-visual-studio-publish-profile.png new file mode 100644 index 000000000..5eb188fc4 Binary files /dev/null and b/samples/react-site-provisioning-manager/assets/functions-visual-studio-publish-profile.png differ diff --git a/samples/react-site-provisioning-manager/assets/screenshot.gif b/samples/react-site-provisioning-manager/assets/screenshot.gif new file mode 100644 index 000000000..bbd7bb91d Binary files /dev/null and b/samples/react-site-provisioning-manager/assets/screenshot.gif differ diff --git a/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp.sln b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp.sln new file mode 100644 index 000000000..fec277de1 --- /dev/null +++ b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29123.88 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProvisioningApp", "ProvisioningApp\ProvisioningApp.csproj", "{8CB1D773-D3D0-4B37-B310-BA94F2476D75}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8CB1D773-D3D0-4B37-B310-BA94F2476D75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CB1D773-D3D0-4B37-B310-BA94F2476D75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CB1D773-D3D0-4B37-B310-BA94F2476D75}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CB1D773-D3D0-4B37-B310-BA94F2476D75}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5374BAB8-D4AB-4D3B-8A38-1C351CF3BB4B} + EndGlobalSection +EndGlobal diff --git a/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/.gitignore b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/.gitignore new file mode 100644 index 000000000..ff5b00c50 --- /dev/null +++ b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Cert/Copy your pfx file here.txt b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Cert/Copy your pfx file here.txt new file mode 100644 index 000000000..be49571f9 --- /dev/null +++ b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Cert/Copy your pfx file here.txt @@ -0,0 +1 @@ +Copy your pfx file here before publish the function. \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Constants/Configs.cs b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Constants/Configs.cs new file mode 100644 index 000000000..7d4d0faa6 --- /dev/null +++ b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Constants/Configs.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ProvisioningApp.Constants +{ + public static class Configs + { + public const string FileName = "PnPProvisioningTemplate.xml"; + public const string clientIdKey = "CLIENTID"; + public const string certificatePathKey = "CERTIFICATE"; + public const string passwordKey = "PASSWORD"; + public const string tenantKey = "TENANT"; + } +} diff --git a/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Functions/ApplyProvisioningTemplate.cs b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Functions/ApplyProvisioningTemplate.cs new file mode 100644 index 000000000..3670ca1be --- /dev/null +++ b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Functions/ApplyProvisioningTemplate.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Host; +using Microsoft.SharePoint.Client; +using OfficeDevPnP.Core.Framework.Provisioning.Model; +using OfficeDevPnP.Core.Framework.Provisioning.Providers.Xml; +using ProvisioningApp.Models; +using ProvisioningApp.Utils; + +namespace ProvisioningApp.Functions +{ + public static class ApplyProvisioningTemplate + { + [FunctionName("ApplyProvisioningTemplate")] + public static async Task Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, TraceWriter log, ExecutionContext context) + { + log.Info("C# HTTP trigger function processed a request."); + + try + { + var requestBody = await req.Content.ReadAsAsync(); + + if (requestBody == null) + return req.CreateResponse(HttpStatusCode.BadRequest); + + var ctx = Helper.GetADAppOnlyContext(requestBody.WebUrl, context.FunctionAppDirectory); + using (ctx) + { + Web web = ctx.Web; + ctx.Load(web, w => w.Title); + ctx.ExecuteQueryRetry(); + + // Configure the XML file system provider + XMLTemplateProvider provider = + new XMLFileSystemTemplateProvider(Path.GetTempPath(), ""); + + byte[] byteArray = Encoding.ASCII.GetBytes(requestBody.Template); + MemoryStream stream = new MemoryStream(byteArray); + + // Load the template from the XML stored copy + ProvisioningTemplate template = provider.GetTemplate(stream); + + // We can also use Apply-PnPProvisioningTemplate + web.ApplyProvisioningTemplate(template); + } + + return req.CreateErrorResponse(HttpStatusCode.OK, "Done!"); + + } + catch (Exception ex) + { + return req.CreateErrorResponse(System.Net.HttpStatusCode.InternalServerError, ex.Message); + } + + } + } +} diff --git a/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Functions/GetProvisioningTemplate.cs b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Functions/GetProvisioningTemplate.cs new file mode 100644 index 000000000..6fc3011ad --- /dev/null +++ b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Functions/GetProvisioningTemplate.cs @@ -0,0 +1,86 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.Azure.WebJobs.Host; +using Microsoft.SharePoint.Client; +using OfficeDevPnP.Core; +using OfficeDevPnP.Core.Framework.Provisioning.Connectors; +using OfficeDevPnP.Core.Framework.Provisioning.Model; +using OfficeDevPnP.Core.Framework.Provisioning.ObjectHandlers; +using OfficeDevPnP.Core.Framework.Provisioning.Providers.Xml; +using ProvisioningApp.Models; +using ProvisioningApp.Constants; +using ProvisioningApp.Utils; +using System.IO; + +namespace ProvisioningApp +{ + public static class GetProvisioningTemplate + { + [FunctionName("GetProvisioningTemplate")] + public static async Task Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, TraceWriter log, ExecutionContext context) + { + log.Info("C# HTTP trigger function processed a request."); + + try + { + var requestBody = await req.Content.ReadAsAsync(); + + if (requestBody == null) + return req.CreateResponse(System.Net.HttpStatusCode.BadRequest); + + GenerateProvisioningTemplate(requestBody,context.FunctionAppDirectory); + log.Info("Template has been created"); + var xDocument = XDocument.Load($"{Path.GetTempPath()}\\{Configs.FileName}"); + // convert the xml into string + string xml = xDocument.ToString(); + var result = new HttpResponseMessage(System.Net.HttpStatusCode.OK); + result.Content = new ByteArrayContent(System.Text.Encoding.UTF8.GetBytes(xml)); + result.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml"); + + return result; + + } + catch (Exception ex) + { + return req.CreateErrorResponse(System.Net.HttpStatusCode.InternalServerError,ex.Message); + } + + } + + private static void GenerateProvisioningTemplate(LoadProvisioningInfo info, string appDirectory) + { + var context = Helper.GetADAppOnlyContext(info.WebUrl, appDirectory); + + using (context) + { + Web web = context.Web; + context.Load(web, w => w.Title); + context.ExecuteQueryRetry(); + ProvisioningTemplateCreationInformation ptci + = new ProvisioningTemplateCreationInformation(context.Web); + + // Create FileSystemConnector to store a temporary copy of the template + ptci.FileConnector = new FileSystemConnector(Path.GetTempPath(), ""); + ptci.PersistBrandingFiles = true; + + ptci.HandlersToProcess = info.Handlers; + // Execute actual extraction of the template + ProvisioningTemplate template = context.Web.GetProvisioningTemplate(ptci); + + // We can serialize this template to save and reuse it + // Optional step + XMLTemplateProvider provider = + new XMLFileSystemTemplateProvider(Path.GetTempPath(), ""); + provider.SaveAs(template, Configs.FileName); + } + } + + + + } +} diff --git a/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Models/ApplyProvisioningInfo.cs b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Models/ApplyProvisioningInfo.cs new file mode 100644 index 000000000..39e39bddc --- /dev/null +++ b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Models/ApplyProvisioningInfo.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ProvisioningApp.Models +{ + public class ApplyProvisioningInfo + { + public string WebUrl { get; set; } + + public string Template { get; set; } + } +} diff --git a/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Models/LoadProvisioningInfo.cs b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Models/LoadProvisioningInfo.cs new file mode 100644 index 000000000..15834ef71 --- /dev/null +++ b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Models/LoadProvisioningInfo.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; +using OfficeDevPnP.Core.Framework.Provisioning.Model; +namespace ProvisioningApp.Models +{ + public class LoadProvisioningInfo + { + public string WebUrl { get; set; } + + public Handlers Handlers { get; set; } + + } +} diff --git a/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Properties/PublishProfiles/Site-Provisioning-App - Zip Deploy.pubxml b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Properties/PublishProfiles/Site-Provisioning-App - Zip Deploy.pubxml new file mode 100644 index 000000000..1eabe6b34 --- /dev/null +++ b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Properties/PublishProfiles/Site-Provisioning-App - Zip Deploy.pubxml @@ -0,0 +1,18 @@ + + + + + $Site-Provisioning-App + ZipDeploy + AzureWebSite + Debug + Any CPU + https://site-provisioning-app.azurewebsites.net + False + /subscriptions/ab4ec88f-687a-4544-847c-84d463e8538b/resourceGroups/Lab/providers/Microsoft.Web/sites/Site-Provisioning-App + <_SavePWD>True + https://site-provisioning-app.scm.azurewebsites.net/ + net461 + True + + \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/ProvisioningApp.csproj b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/ProvisioningApp.csproj new file mode 100644 index 000000000..8e40006f2 --- /dev/null +++ b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/ProvisioningApp.csproj @@ -0,0 +1,29 @@ + + + net461 + v1 + + + + + + + + + + + + Always + + + PreserveNewest + + + PreserveNewest + Never + + + + + + diff --git a/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Utils/Helper.cs b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Utils/Helper.cs new file mode 100644 index 000000000..95e8e6a17 --- /dev/null +++ b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/Utils/Helper.cs @@ -0,0 +1,35 @@ +using Microsoft.SharePoint.Client; +using OfficeDevPnP.Core; +using System; +using ProvisioningApp.Constants; + +namespace ProvisioningApp.Utils +{ + public static class Helper + { + public static ClientContext GetADAppOnlyContext(string siteUrl, string appDirectory) + { + var authMgr = new AuthenticationManager(); + + string certificateName = Environment.GetEnvironmentVariable(Configs.certificatePathKey); + string password = Environment.GetEnvironmentVariable(Configs.passwordKey); + string clientId = Environment.GetEnvironmentVariable(Configs.clientIdKey); + string tenant = Environment.GetEnvironmentVariable(Configs.tenantKey); + string certificatePath = $"{appDirectory}\\Cert\\{certificateName}"; + + if (string.IsNullOrEmpty(clientId)) + throw new ArgumentException($"Missing required environment variable '{Configs.clientIdKey}'"); + + if (string.IsNullOrEmpty(certificateName)) + throw new ArgumentException($"Missing required environment variable '{Configs.certificatePathKey}'"); + + if (string.IsNullOrEmpty(password)) + throw new ArgumentException($"Missing required environment variable '{Configs.passwordKey}'"); + + if (string.IsNullOrEmpty(tenant)) + throw new ArgumentException($"Missing required environment variable '{Configs.tenantKey}'"); + + return authMgr.GetAzureADAppOnlyAuthenticatedContext(siteUrl, clientId, tenant, certificatePath, password); + } + } +} diff --git a/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/host.json b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/host.json new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/host.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/.editorconfig b/samples/react-site-provisioning-manager/webpart/.editorconfig new file mode 100644 index 000000000..8ffcdc4ec --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/.editorconfig @@ -0,0 +1,25 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# change these settings to your own preference +indent_style = space +indent_size = 2 + +# we recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[{package,bower}.json] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/.gitignore b/samples/react-site-provisioning-manager/webpart/.gitignore new file mode 100644 index 000000000..b19bbe123 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/.gitignore @@ -0,0 +1,32 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules + +# Build generated files +dist +lib +solution +temp +*.sppkg + +# Coverage directory used by tools like istanbul +coverage + +# OSX +.DS_Store + +# Visual Studio files +.ntvs_analysis.dat +.vs +bin +obj + +# Resx Generated Code +*.resx.ts + +# Styles Generated Code +*.scss.ts diff --git a/samples/react-site-provisioning-manager/webpart/.yo-rc.json b/samples/react-site-provisioning-manager/webpart/.yo-rc.json new file mode 100644 index 000000000..1023b3dfc --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/.yo-rc.json @@ -0,0 +1,12 @@ +{ + "@microsoft/generator-sharepoint": { + "isCreatingSolution": true, + "environment": "spo", + "version": "1.9.1", + "libraryName": "react-site-provisioning-manager", + "libraryId": "5df6f62a-b141-4997-8944-029a1fd73237", + "packageManager": "npm", + "isDomainIsolated": false, + "componentType": "webpart" + } +} \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/config/config.json b/samples/react-site-provisioning-manager/webpart/config/config.json new file mode 100644 index 000000000..a5ed38207 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/config/config.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", + "version": "2.0", + "bundles": { + "site-provisioning-manager-web-part": { + "components": [ + { + "entrypoint": "./lib/webparts/siteProvisioningManager/SiteProvisioningManagerWebPart.js", + "manifest": "./src/webparts/siteProvisioningManager/SiteProvisioningManagerWebPart.manifest.json" + } + ] + } + }, + "externals": {}, + "localizedResources": { + "SiteProvisioningManagerWebPartStrings": "lib/webparts/siteProvisioningManager/loc/{locale}.js" + } +} diff --git a/samples/react-site-provisioning-manager/webpart/config/copy-assets.json b/samples/react-site-provisioning-manager/webpart/config/copy-assets.json new file mode 100644 index 000000000..3771fd04a --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/config/copy-assets.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json", + "deployCdnPath": "temp/deploy" +} diff --git a/samples/react-site-provisioning-manager/webpart/config/deploy-azure-storage.json b/samples/react-site-provisioning-manager/webpart/config/deploy-azure-storage.json new file mode 100644 index 000000000..0d283c1a8 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/config/deploy-azure-storage.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", + "workingDir": "./temp/deploy/", + "account": "", + "container": "react-site-provisioning-manager", + "accessKey": "" +} \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/config/package-solution.json b/samples/react-site-provisioning-manager/webpart/config/package-solution.json new file mode 100644 index 000000000..3fce9e132 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/config/package-solution.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", + "solution": { + "name": "react-site-provisioning-manager-client-side-solution", + "id": "5df6f62a-b141-4997-8944-029a1fd73237", + "version": "1.0.0.0", + "includeClientSideAssets": true, + "skipFeatureDeployment": true, + "webApiPermissionRequests": [{ + "resource": "Site-Provisioning-App", + "scope": "user_impersonation" + }, + { + "resource": "Microsoft Graph", + "scope": "Directory.Read.All" + }], + "isDomainIsolated": false + }, + "paths": { + "zippedPackage": "solution/react-site-provisioning-manager.sppkg" + } +} diff --git a/samples/react-site-provisioning-manager/webpart/config/serve.json b/samples/react-site-provisioning-manager/webpart/config/serve.json new file mode 100644 index 000000000..090cfe9e6 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/config/serve.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json", + "port": 4321, + "https": true, + "initialPage": "https://localhost:5432/workbench", + "api": { + "port": 5432, + "entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/" + } +} diff --git a/samples/react-site-provisioning-manager/webpart/config/write-manifests.json b/samples/react-site-provisioning-manager/webpart/config/write-manifests.json new file mode 100644 index 000000000..bad352605 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/config/write-manifests.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json", + "cdnBasePath": "" +} \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/gulpfile.js b/samples/react-site-provisioning-manager/webpart/gulpfile.js new file mode 100644 index 000000000..7958fd5d8 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/gulpfile.js @@ -0,0 +1,7 @@ +'use strict'; + +const gulp = require('gulp'); +const build = require('@microsoft/sp-build-web'); +build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); + +build.initialize(gulp); diff --git a/samples/react-site-provisioning-manager/webpart/package.json b/samples/react-site-provisioning-manager/webpart/package.json new file mode 100644 index 000000000..b28f71d14 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/package.json @@ -0,0 +1,46 @@ +{ + "name": "react-site-provisioning-manager", + "version": "0.0.1", + "private": true, + "main": "lib/index.js", + "engines": { + "node": ">=0.10.0" + }, + "scripts": { + "build": "gulp bundle", + "clean": "gulp clean", + "test": "gulp test" + }, + "dependencies": { + "@microsoft/sp-core-library": "1.9.1", + "@microsoft/sp-lodash-subset": "1.9.1", + "@microsoft/sp-office-ui-fabric-core": "1.9.1", + "@microsoft/sp-webpart-base": "1.9.1", + "@pnp/common": "^1.3.4", + "@pnp/graph": "^1.3.4", + "@pnp/logging": "^1.3.4", + "@pnp/odata": "^1.3.4", + "@pnp/sp": "^1.3.4", + "@types/es6-promise": "0.0.33", + "@types/react": "16.8.8", + "@types/react-dom": "16.8.3", + "@types/webpack-env": "1.13.1", + "office-ui-fabric-react": "6.189.2", + "react": "16.8.5", + "react-dom": "16.8.5" + }, + "resolutions": { + "@types/react": "16.8.8" + }, + "devDependencies": { + "@microsoft/sp-build-web": "1.9.1", + "@microsoft/sp-tslint-rules": "1.9.1", + "@microsoft/sp-module-interfaces": "1.9.1", + "@microsoft/sp-webpart-workbench": "1.9.1", + "@microsoft/rush-stack-compiler-2.9": "0.7.16", + "gulp": "~3.9.1", + "@types/chai": "3.4.34", + "@types/mocha": "2.2.38", + "ajv": "~5.2.2" + } +} diff --git a/samples/react-site-provisioning-manager/webpart/src/index.ts b/samples/react-site-provisioning-manager/webpart/src/index.ts new file mode 100644 index 000000000..fb81db1e2 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/index.ts @@ -0,0 +1 @@ +// A file is required to be in the root of the /src directory by the TypeScript compiler diff --git a/samples/react-site-provisioning-manager/webpart/src/models/Handlers.ts b/samples/react-site-provisioning-manager/webpart/src/models/Handlers.ts new file mode 100644 index 000000000..d7f361e93 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/models/Handlers.ts @@ -0,0 +1,67 @@ +export interface IHandlers{ + All:boolean; + AuditSettings:boolean; + ComposedLook:boolean; + CustomActions:boolean; + ExtensibilityProviders:boolean; + Features:boolean; + Fields:boolean; + Files:boolean; + Lists:boolean; + Pages:boolean; + Publishing:boolean; + RegionalSettings:boolean; + SearchSettings:boolean; + SitePolicy:boolean; + SupportedUILanguages:boolean; + TermGroups:boolean; + Workflows:boolean; + SiteSecurity:boolean; + ContentTypes:boolean; + PropertyBagEntries:boolean; + PageContents:boolean; + WebSettings:boolean; + Navigation:boolean; + ImageRenditions:boolean; + ApplicationLifecycleManagement:boolean; + Tenant:boolean; + WebApiPermissions:boolean; + SiteHeader:boolean; + SiteFooter:boolean; + Theme:boolean; +} + +const defaultHandlerValues:IHandlers={ + All:false, + AuditSettings:false, + ComposedLook:false, + CustomActions:false, + ExtensibilityProviders:false, + Features:false, + Fields:false, + Files:false, + Lists:false, + Pages:false, + Publishing:false, + RegionalSettings:false, + SearchSettings:false, + SitePolicy:false, + SupportedUILanguages:false, + TermGroups:false, + Workflows:false, + SiteSecurity:false, + ContentTypes:false, + PropertyBagEntries:false, + PageContents:false, + WebSettings:false, + Navigation:false, + ImageRenditions:false, + ApplicationLifecycleManagement:false, + Tenant:false, + WebApiPermissions:false, + SiteFooter:false, + SiteHeader:false, + Theme:false +}; + +export default defaultHandlerValues; \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/src/services/appService.ts b/samples/react-site-provisioning-manager/webpart/src/services/appService.ts new file mode 100644 index 000000000..8335143ba --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/services/appService.ts @@ -0,0 +1,71 @@ +import { AadHttpClient, IHttpClientOptions, HttpClientResponse } from "@microsoft/sp-http"; +import httpHeaders from "./headers"; +import { graph } from "@pnp/graph"; +import { sp } from "@pnp/sp"; +import { WebPartContext } from "@microsoft/sp-webpart-base"; + +export default class AppService { + private ADMIN_ROLETEMPLATE_ID = "62e90394-69f5-4237-9190-012177145e10"; // Global Admin TemplateRoleId + + private requestOptions: IHttpClientOptions = { + headers: httpHeaders, + }; + + constructor(private spfxContext: WebPartContext, + private httpClient: AadHttpClient, + private getProvisioningTemplateUrl, + private applyProvisioningTemplateUrl) { + // Setuo Context to PnPjs and MSGraph + sp.setup({ + spfxContext: this.spfxContext + }); + + graph.setup({ + spfxContext: this.spfxContext + }); + + } + // Check if user is Global Admin + public async checkUserIsGlobalAdmin(): Promise { + return graph.me.memberOf.get().then(roles =>{ + for (const myDirRolesAndGroup of roles) { + if (myDirRolesAndGroup.id && myDirRolesAndGroup.id === this.ADMIN_ROLETEMPLATE_ID) { // roleTemplateId for glabal Admin + return true; + } + } + return false; + + }).catch( e => {return false;}); + } + + /** + * Check if the current user is a site admin + */ + public async IsSiteOwner(): Promise { + return sp.web.currentUser.get().then(user => { + return user.IsSiteAdmin; + }).catch((error: any) => { + return false; + }); + } + + public async GetProvisioningTemplate(url: string, handlers: string): Promise { + this.requestOptions.body = `{ WebUrl: '${url}', Handlers: '${handlers}' }`; + return this.httpClient.post( + this.getProvisioningTemplateUrl, + AadHttpClient.configurations.v1, + this.requestOptions, + ); + } + + public async ApplyProvisioningTemplate(url: string, template: string): Promise { + this.requestOptions.body = `{ WebUrl: '${url}', Template: '${template}' }`; + return this.httpClient.post( + this.applyProvisioningTemplateUrl, + AadHttpClient.configurations.v1, + this.requestOptions, + ); + } + + +} \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/src/services/headers.ts b/samples/react-site-provisioning-manager/webpart/src/services/headers.ts new file mode 100644 index 000000000..1c1626cd0 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/services/headers.ts @@ -0,0 +1,4 @@ +const headers : Headers= new Headers(); +headers.append("Accept","application/json"); +headers.append("Content-type","application/json"); +export default headers; \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/SiteProvisioningManagerWebPart.manifest.json b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/SiteProvisioningManagerWebPart.manifest.json new file mode 100644 index 000000000..fe4142946 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/SiteProvisioningManagerWebPart.manifest.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json", + "id": "c2fdd0d3-2636-42e9-9f39-c2ee831b0c5c", + "alias": "SiteProvisioningManagerWebPart", + "componentType": "WebPart", + "version": "*", + "manifestVersion": 2, + "requiresCustomScript": false, + "supportedHosts": ["SharePointWebPart"], + "preconfiguredEntries": [{ + "groupId": "5c03119e-3074-46fd-976b-c60198311f70", + "group": { "default": "Other" }, + "title": { "default": "Site Provisioning Manager" }, + "description": { "default": "Get or apply PnP provisioning templates" }, + "officeFabricIconFontName": "CustomizeToolbar", + "properties": { + "description": "Site Provisioning Manager", + "ApplicationId": "", + "GetTemplateFunctionUrl": "", + "ApplyTemplateFunctionUrl": "" + } + }] +} diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/SiteProvisioningManagerWebPart.ts b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/SiteProvisioningManagerWebPart.ts new file mode 100644 index 000000000..7bea1c7bf --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/SiteProvisioningManagerWebPart.ts @@ -0,0 +1,83 @@ +import * as React from 'react'; +import * as ReactDom from 'react-dom'; +import { Version } from '@microsoft/sp-core-library'; +import { + BaseClientSideWebPart, + IPropertyPaneConfiguration, + PropertyPaneTextField +} from '@microsoft/sp-webpart-base'; + +import * as strings from 'SiteProvisioningManagerWebPartStrings'; +import SiteProvisioningWebPart from './components/App/App'; +import { IAppProps } from './components/App/IAppProps'; +import { AadHttpClient } from '@microsoft/sp-http'; +import AppService from '../../services/appService'; + +export interface ISiteProvisioningManagerWebPartProps { + ApplicationId: string; + GetTemplateFunctionUrl: string; + ApplyTemplateFunctionUrl: string; +} + +export default class SiteProvisioningManagerWebPart extends BaseClientSideWebPart { + + private appService: AppService; + private aadClient: AadHttpClient; + public onInit(): Promise { + return super.onInit().then(async _ => { + const { GetTemplateFunctionUrl, ApplyTemplateFunctionUrl } = this.properties; + + const clientId: string = this.properties.ApplicationId; + this.aadClient = await this.context.aadHttpClientFactory.getClient(clientId); + this.appService = new AppService(this.context, this.aadClient, GetTemplateFunctionUrl, ApplyTemplateFunctionUrl); + }); + + } + public render(): void { + const element: React.ReactElement = React.createElement( + SiteProvisioningWebPart, + { + appService: this.appService, + webUrl: this.context.pageContext.web.absoluteUrl + } + ); + + ReactDom.render(element, this.domElement); + } + + protected onDispose(): void { + ReactDom.unmountComponentAtNode(this.domElement); + } + + protected get dataVersion(): Version { + return Version.parse('1.0'); + } + + protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { + return { + pages: [ + { + header: { + description: strings.PropertyPaneDescription + }, + groups: [ + { + groupName: strings.BasicGroupName, + groupFields: [ + PropertyPaneTextField('ApplicationId', { + label: strings.ClientIdFieldLabel + }), + PropertyPaneTextField('GetTemplateFunctionUrl', { + label: strings.GetProvisioningUrlFieldLabel + }), + PropertyPaneTextField('ApplyTemplateFunctionUrl', { + label: strings.ApplyProvisioningUrlFieldLabel + }) + ] + } + ] + } + ] + }; + } +} diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/App.module.scss b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/App.module.scss new file mode 100644 index 000000000..cfaae915c --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/App.module.scss @@ -0,0 +1,44 @@ +@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss'; + +.siteProvisioningWebPart { + .container { + max-width: 700px; + margin: 0px auto; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); + } + + .row { + @include ms-Grid-row; + } + .loadingImage{ + width: 150px; + height: 140px; + } + .column { + @include ms-Grid-col; + @include ms-lg6; + @include ms-sm6; + @include ms-md6; + @include ms-xl6; + } + .handlerCheckbox{ + margin-top:5px; + } + .topMargin{ + margin-top:10px; + } + .pivotContainer{ + margin-left:10px; + } + .flexRow{ + width: 100%; + display: flex; + } + .provisioningButton{ + margin-top:10px; + margin-right: 10px; + } + .error{ + margin-top:10px; + } +} \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/App.tsx b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/App.tsx new file mode 100644 index 000000000..bceb8ed29 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/App.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import styles from './App.module.scss'; +import { IAppProps } from './IAppProps'; +import AppContext,{IMessageBarSettings} from "./AppContext"; +import { MessageBarType } from 'office-ui-fabric-react/lib/MessageBar'; +import AppContent from "./AppContent"; +import * as Strings from "SiteProvisioningManagerWebPartStrings"; + +const App: React.FC = (props) => { + const {webUrl,appService} = props; + const [isGlobalAdmin,setIsGlobalAdmin] = React.useState(false); + const [isSiteOwner,setIsSiteOwner] = React.useState(false); + const [isLoading,setIsLoading] = React.useState(false); + const [messageBarSettings,setMessageBarSettings] =React.useState({ + message:"", + type:MessageBarType.info, + visible:false + } as IMessageBarSettings); + + + const updateMessageBarSettings = (settings:IMessageBarSettings)=>{ + setMessageBarSettings(settings); + }; + + const toggleLoading = (visibleLoading:boolean)=>{ + setIsLoading(visibleLoading); + }; + + React.useEffect(()=>{ + let didCancel = false; + + const fetchIsGloablAdmin = async ()=>{ + const globalAdmin = await appService.checkUserIsGlobalAdmin(); + if (!didCancel) { + setIsGlobalAdmin(globalAdmin); + } + + }; + + const fetchIsSiteOwner = async ()=>{ + const siteOwner = await appService.IsSiteOwner(); + if (!didCancel) { + setIsSiteOwner(siteOwner); + if(!siteOwner){ + setMessageBarSettings({ + message: Strings.ErrorMessageUserNotAdmin, + type: MessageBarType.error, + visible: false + }); + } + } + }; + + fetchIsGloablAdmin(); + fetchIsSiteOwner(); + + return ()=>{didCancel=true;}; + },[]); + + return ( +
+ + + +
+ ); +}; + +export default App; \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/AppContent.tsx b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/AppContent.tsx new file mode 100644 index 000000000..95c1dcc09 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/AppContent.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar'; +import Loading from '../Loading/Loading'; +import { Pivot, PivotItem, PivotLinkSize } from 'office-ui-fabric-react/lib/Pivot'; +import GetProvisioningTemplate from '../GetProvisioningTemplate/GetProvisioningTemplate'; +import ApplyProvisioningTemplate from '../ApplyProvisioningTemplate/ApplyProvisioningTemplate'; +import AppContext from './AppContext'; +import * as Strings from "SiteProvisioningManagerWebPartStrings"; +import styles from './App.module.scss'; + +const AppContent = () => { +const ctx = React.useContext(AppContext); +const {isLoading,messageBarSettings:{type,message,visible}} = ctx; + + return ( +
+ +
+ ); +}; + +export default AppContent; \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/AppContext.tsx b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/AppContext.tsx new file mode 100644 index 000000000..cffe3fc8a --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/AppContext.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { MessageBarType } from 'office-ui-fabric-react/lib/MessageBar'; +import AppService from '../../../../services/appService'; + +export interface IMessageBarSettings{ + visible:boolean; + message: string; + type: MessageBarType; + } + + interface IAppContextInterface { + appService: AppService; + isGlobalAdmin: boolean; + isSiteOwner: boolean; + webUrl: string; + messageBarSettings: IMessageBarSettings; + isLoading:boolean; + toggleLoading: (visible:boolean)=>void; + updateMessageBarSettings: (settings:IMessageBarSettings)=>void; + } + +const AppContext = React.createContext(null); +export default AppContext; diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/IAppProps.ts b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/IAppProps.ts new file mode 100644 index 000000000..4f9b3e31f --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/App/IAppProps.ts @@ -0,0 +1,6 @@ +import AppService from "../../../../services/appService"; + +export interface IAppProps { + appService: AppService; + webUrl:string; +} \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/ApplyProvisioningTemplate/ApplyProvisioningTemplate.tsx b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/ApplyProvisioningTemplate/ApplyProvisioningTemplate.tsx new file mode 100644 index 000000000..1eda896eb --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/ApplyProvisioningTemplate/ApplyProvisioningTemplate.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import styles from "../App/App.module.scss"; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import { HttpClientResponse } from '@microsoft/sp-http'; +import AppContext from "../App/AppContext"; +import { MessageBarType } from 'office-ui-fabric-react/lib/MessageBar'; +import * as Strings from "SiteProvisioningManagerWebPartStrings"; + +const SiteProvisioningTemplate = () => { + const ctx = React.useContext(AppContext); + const [template, setTemplate] = React.useState(""); + const [webUrl, setWebUrl] = React.useState(ctx.webUrl); + const isNotAdmin = !ctx.isGlobalAdmin || !ctx.isSiteOwner; + const applyTemplate = async () => { + ctx.toggleLoading(true); + ctx.updateMessageBarSettings({ + message: "", + type: MessageBarType.error, + visible: false + }); + + const response: HttpClientResponse = await ctx.appService.ApplyProvisioningTemplate(webUrl, template); + const responseText = await response.text(); + if (response.status === 200) + ctx.updateMessageBarSettings({ + message: Strings.SuccessMessage, + type: MessageBarType.success, + visible: true + }); + else + ctx.updateMessageBarSettings({ + message: responseText, + type: MessageBarType.error, + visible: true + }); + + ctx.toggleLoading(false); + }; + + return ( +
+ +
+ ); +}; + +export default SiteProvisioningTemplate; \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/Assets/loading_dots.gif b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/Assets/loading_dots.gif new file mode 100644 index 000000000..e181e248d Binary files /dev/null and b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/Assets/loading_dots.gif differ diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/GetProvisioningTemplate/GetProvisioningTemplate.tsx b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/GetProvisioningTemplate/GetProvisioningTemplate.tsx new file mode 100644 index 000000000..fe22d36d4 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/GetProvisioningTemplate/GetProvisioningTemplate.tsx @@ -0,0 +1,109 @@ +import * as React from "react"; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import Handlers from './Handlers'; +import styles from "../App/App.module.scss"; +import { Label } from "office-ui-fabric-react/lib/Label"; +import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import AppContext from "../App/AppContext"; +import HandlerValues from "../../../../models/Handlers"; +import { HttpClientResponse } from "@microsoft/sp-http"; +import { MessageBarType } from "office-ui-fabric-react/lib/MessageBar"; + +const GetProvisioningTemplate = () => { + const ctx = React.useContext(AppContext); + const [handlers, setHandlers] = React.useState([]); + const [webUrl, setWebUrl] = React.useState(ctx.webUrl); + const [template, setTemplate] = React.useState(""); + const [handlerValues, setHandlerValues] = React.useState(HandlerValues); + const isNotAdmin = !ctx.isGlobalAdmin || !ctx.isSiteOwner; + + const resetHandlers = () => { + const allHandlers = Object.keys(HandlerValues); + let newValues = HandlerValues; + allHandlers.map(value => newValues[value] = false); + setHandlerValues(newValues); + }; + + const onHandlerChange = (ev: React.FormEvent, isChecked: boolean) => { + const handlerName = ev.currentTarget.getAttribute("name"); + + if (handlerName === "All") { + if (isChecked) { + setHandlers(["All"]); + setHandlerValues({ ...handlerValues, [handlerName]: isChecked }); + } + + else { + resetHandlers(); + setHandlers([]); + } + } + else { + let newHandlers = [...handlers]; + if (isChecked) + newHandlers = newHandlers.concat(handlerName); + else + newHandlers = newHandlers.filter(h => h != handlerName); + setHandlers(newHandlers); + setHandlerValues({ ...handlerValues, [handlerName]: isChecked }); + } + }; + + const getTemplate = async () => { + ctx.toggleLoading(true); + ctx.updateMessageBarSettings({ + message: "", + type: MessageBarType.success, + visible: false + }); + const response: HttpClientResponse = await ctx.appService.GetProvisioningTemplate(webUrl, handlers.join(",")); + + const responseText = await response.text(); + if (response.status === 200) + setTemplate(responseText); + else + ctx.updateMessageBarSettings({ + message: responseText, + type: MessageBarType.error, + visible: true + }); + ctx.toggleLoading(false); + }; + + const downloadTemplate = () => { + if (template !== "") { + const fileName = "PnPProvisioningTemplate.xml"; + const fileContent = new Blob([template], { type: 'text/plain' }); + const downloadTag = document.createElement("a"); + downloadTag.setAttribute("href", window.URL.createObjectURL(fileContent)); + downloadTag.setAttribute("download", fileName); + downloadTag.dataset.downloadurl = ['text/plain', downloadTag.download, downloadTag.href].join(':'); + downloadTag.draggable = true; + downloadTag.classList.add('dragour'); + downloadTag.click(); + } + }; + + return ( +
+ + +
+ ); +}; + +export default GetProvisioningTemplate; \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/GetProvisioningTemplate/Handlers.tsx b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/GetProvisioningTemplate/Handlers.tsx new file mode 100644 index 000000000..da3bbb2b2 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/GetProvisioningTemplate/Handlers.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; +import styles from "../App/App.module.scss"; +import { IHandlers } from "../../../../models/Handlers"; + +interface IHandlerProps { + onHandlerChange: (ev: React.FormEvent, isChecked: boolean) => void; + values: IHandlers; + disabled:boolean; +} + +const Handlers: React.FC = (props) => { + const { values } = props; + const allHandlers = Object.keys(values); + const checkboxes = allHandlers.map((value) => + + ); + return ( +
+
+
+ { + checkboxes.slice(0, 15) + } +
+
+ { + checkboxes.slice(15, 30) + } +
+
+
+ ); +}; + +export default Handlers; \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/Loading/Loading.tsx b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/Loading/Loading.tsx new file mode 100644 index 000000000..8e32fa7f7 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/components/Loading/Loading.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; +import styles from "../App/App.module.scss"; +const loadingImage: any = require("../Assets/loading_dots.gif"); + +interface ILoadingProps{ + hidden:boolean; +} + +const Loading: React.FC = (props) => { + return ( + + ); +}; + +export default Loading; \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/loc/en-us.js b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/loc/en-us.js new file mode 100644 index 000000000..50b1e9cd1 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/loc/en-us.js @@ -0,0 +1,15 @@ +define([], function() { + return { + "PropertyPaneDescription": "Site provisioning manager", + "BasicGroupName": "Azure functions configuration", + "DescriptionFieldLabel": "Description Field", + "ErrorMessageUserNotAdmin": "You are not Site Admin to manage provisioning templates.", + "LoadingLabel": "Please wait, this process takes a while...", + "SuccessMessage": "Provisioning template has been applied successfully.", + "GetTemplateLabel":"Get Template", + "ApplyTemplateLable":"Apply Template", + "ClientIdFieldLabel":"Application Id/Endpoint", + "GetProvisioningUrlFieldLabel": "Get provisioning function URL", + "ApplyProvisioningUrlFieldLabel": "Apply Provisioning function URL" + } +}); \ No newline at end of file diff --git a/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/loc/mystrings.d.ts b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/loc/mystrings.d.ts new file mode 100644 index 000000000..75887717f --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/src/webparts/siteProvisioningManager/loc/mystrings.d.ts @@ -0,0 +1,18 @@ + +declare interface ISiteProvisioningManagerWebPartStrings { + PropertyPaneDescription: string; + BasicGroupName: string; + ClientIdFieldLabel: string; + ErrorMessageUserNotAdmin:string; + LoadingLabel:string; + SuccessMessage:string; + GetTemplateLabel:string; + ApplyTemplateLable:string; + GetProvisioningUrlFieldLabel:string; + ApplyProvisioningUrlFieldLabel:string; +} + +declare module 'SiteProvisioningManagerWebPartStrings' { + const strings: ISiteProvisioningManagerWebPartStrings; + export = strings; +} diff --git a/samples/react-site-provisioning-manager/webpart/teams/c2fdd0d3-2636-42e9-9f39-c2ee831b0c5c_color.png b/samples/react-site-provisioning-manager/webpart/teams/c2fdd0d3-2636-42e9-9f39-c2ee831b0c5c_color.png new file mode 100644 index 000000000..a8d279707 Binary files /dev/null and b/samples/react-site-provisioning-manager/webpart/teams/c2fdd0d3-2636-42e9-9f39-c2ee831b0c5c_color.png differ diff --git a/samples/react-site-provisioning-manager/webpart/teams/c2fdd0d3-2636-42e9-9f39-c2ee831b0c5c_outline.png b/samples/react-site-provisioning-manager/webpart/teams/c2fdd0d3-2636-42e9-9f39-c2ee831b0c5c_outline.png new file mode 100644 index 000000000..6df4a038d Binary files /dev/null and b/samples/react-site-provisioning-manager/webpart/teams/c2fdd0d3-2636-42e9-9f39-c2ee831b0c5c_outline.png differ diff --git a/samples/react-site-provisioning-manager/webpart/tsconfig.json b/samples/react-site-provisioning-manager/webpart/tsconfig.json new file mode 100644 index 000000000..131c5bb80 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/tsconfig.json @@ -0,0 +1,38 @@ +{ + "extends": "./node_modules/@microsoft/rush-stack-compiler-2.9/includes/tsconfig-web.json", + "compilerOptions": { + "target": "es5", + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "declaration": true, + "sourceMap": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "outDir": "lib", + "inlineSources": false, + "strictNullChecks": false, + "noUnusedLocals": false, + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@microsoft" + ], + "types": [ + "es6-promise", + "webpack-env" + ], + "lib": [ + "es5", + "dom", + "es2015.collection" + ] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "lib" + ] +} diff --git a/samples/react-site-provisioning-manager/webpart/tslint.json b/samples/react-site-provisioning-manager/webpart/tslint.json new file mode 100644 index 000000000..23fa2aa43 --- /dev/null +++ b/samples/react-site-provisioning-manager/webpart/tslint.json @@ -0,0 +1,30 @@ +{ + "extends": "@microsoft/sp-tslint-rules/base-tslint.json", + "rules": { + "class-name": false, + "export-name": false, + "forin": false, + "label-position": false, + "member-access": true, + "no-arg": false, + "no-console": false, + "no-construct": false, + "no-duplicate-variable": true, + "no-eval": false, + "no-function-expression": true, + "no-internal-module": true, + "no-shadowed-variable": true, + "no-switch-case-fall-through": true, + "no-unnecessary-semicolons": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-with-statement": true, + "semicolon": true, + "trailing-comma": false, + "typedef": false, + "typedef-whitespace": false, + "use-named-parameter": true, + "variable-name": false, + "whitespace": false + } +} \ No newline at end of file