Merge pull request #966 from LauraKokkarinen/PR951
Manual merge of PR951
|
@ -0,0 +1,2 @@
|
|||
.vscode
|
||||
/webpart/package-lock.json
|
|
@ -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.
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-site-provisioning-manager" />
|
After Width: | Height: | Size: 97 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 585 KiB |
|
@ -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
|
264
samples/react-site-provisioning-manager/azure-function/ProvisioningApp/ProvisioningApp/.gitignore
vendored
Normal file
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
Copy your pfx file here before publish the function.
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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<HttpResponseMessage> 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<ApplyProvisioningInfo>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<HttpResponseMessage> 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<LoadProvisioningInfo>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<UserName>$Site-Provisioning-App</UserName>
|
||||
<WebPublishMethod>ZipDeploy</WebPublishMethod>
|
||||
<PublishProvider>AzureWebSite</PublishProvider>
|
||||
<LastUsedBuildConfiguration>Debug</LastUsedBuildConfiguration>
|
||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||
<SiteUrlToLaunchAfterPublish>https://site-provisioning-app.azurewebsites.net</SiteUrlToLaunchAfterPublish>
|
||||
<LaunchSiteAfterPublish>False</LaunchSiteAfterPublish>
|
||||
<ResourceId>/subscriptions/ab4ec88f-687a-4544-847c-84d463e8538b/resourceGroups/Lab/providers/Microsoft.Web/sites/Site-Provisioning-App</ResourceId>
|
||||
<_SavePWD>True</_SavePWD>
|
||||
<PublishUrl>https://site-provisioning-app.scm.azurewebsites.net/</PublishUrl>
|
||||
<TargetFramework>net461</TargetFramework>
|
||||
<SkipExtraFilesOnServer>True</SkipExtraFilesOnServer>
|
||||
</PropertyGroup>
|
||||
</Project>
|
|
@ -0,0 +1,29 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net461</TargetFramework>
|
||||
<AzureFunctionsVersion>v1</AzureFunctionsVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.24" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="SharePointPnPCoreOnline" Version="3.12.1908" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="Cert\provisioningapp.pfx">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="host.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="local.settings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Cert\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
{
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-site-provisioning-manager",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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);
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -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;
|
|
@ -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<boolean> {
|
||||
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<boolean> {
|
||||
return sp.web.currentUser.get().then(user => {
|
||||
return user.IsSiteAdmin;
|
||||
}).catch((error: any) => {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public async GetProvisioningTemplate(url: string, handlers: string): Promise<HttpClientResponse> {
|
||||
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<HttpClientResponse> {
|
||||
this.requestOptions.body = `{ WebUrl: '${url}', Template: '${template}' }`;
|
||||
return this.httpClient.post(
|
||||
this.applyProvisioningTemplateUrl,
|
||||
AadHttpClient.configurations.v1,
|
||||
this.requestOptions,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
const headers : Headers= new Headers();
|
||||
headers.append("Accept","application/json");
|
||||
headers.append("Content-type","application/json");
|
||||
export default headers;
|
|
@ -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": ""
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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<ISiteProvisioningManagerWebPartProps> {
|
||||
|
||||
private appService: AppService;
|
||||
private aadClient: AadHttpClient;
|
||||
public onInit(): Promise<void> {
|
||||
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<IAppProps> = 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
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<IAppProps> = (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 (
|
||||
<div className={styles.siteProvisioningWebPart}>
|
||||
<AppContext.Provider value={{
|
||||
appService,
|
||||
isGlobalAdmin,
|
||||
isSiteOwner,
|
||||
webUrl,
|
||||
messageBarSettings,
|
||||
isLoading,
|
||||
toggleLoading,
|
||||
updateMessageBarSettings
|
||||
} }>
|
||||
<AppContent/>
|
||||
</AppContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
|
@ -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 (
|
||||
<div>
|
||||
<div hidden={!visible} className={styles.topMargin}>
|
||||
<MessageBar messageBarType={type}>{message}</MessageBar>
|
||||
</div>
|
||||
<Loading hidden={!isLoading} />
|
||||
<Pivot linkSize={PivotLinkSize.large} hidden={isLoading}>
|
||||
<PivotItem headerText={Strings.GetTemplateLabel}>
|
||||
<GetProvisioningTemplate />
|
||||
</PivotItem>
|
||||
<PivotItem headerText={Strings.ApplyTemplateLable}>
|
||||
<ApplyProvisioningTemplate />
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppContent;
|
|
@ -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<IAppContextInterface | null>(null);
|
||||
export default AppContext;
|
|
@ -0,0 +1,6 @@
|
|||
import AppService from "../../../../services/appService";
|
||||
|
||||
export interface IAppProps {
|
||||
appService: AppService;
|
||||
webUrl:string;
|
||||
}
|
|
@ -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 (
|
||||
<div className={styles.pivotContainer}>
|
||||
<div hidden={ctx.isLoading}>
|
||||
<TextField disabled={!ctx.isGlobalAdmin} label="Site URL" value={webUrl} onChanged={(value) => setWebUrl(value)} />
|
||||
<TextField disabled={!isNotAdmin} label="Template" value={template} onChanged={(value) => setTemplate(value)} multiline rows={20} />
|
||||
<div className={styles.topMargin}>
|
||||
<PrimaryButton disabled={!isNotAdmin} text="Apply Template" onClick={applyTemplate} className={styles.provisioningButton} />
|
||||
<DefaultButton disabled={!isNotAdmin} text="Reset" onClick={() => setTemplate("")} className={styles.provisioningButton} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteProvisioningTemplate;
|
After Width: | Height: | Size: 546 KiB |
|
@ -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<HTMLElement>, 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 (
|
||||
<div className={styles.pivotContainer}>
|
||||
<div hidden={ctx.isLoading || template.length > 1}>
|
||||
<TextField disabled={!ctx.isGlobalAdmin} label="Site URL" value={webUrl} onChanged={(value) => setWebUrl(value)} />
|
||||
<Label>Handlers</Label>
|
||||
<Handlers disabled={!isNotAdmin} values={handlerValues} onHandlerChange={onHandlerChange} />
|
||||
<div className={styles.topMargin}>
|
||||
<PrimaryButton text="Get Template" className={styles.provisioningButton} disabled={(handlers.length < 1 || webUrl.length < 1) || !isNotAdmin} onClick={getTemplate} />
|
||||
<DefaultButton text="Reset" disabled={!isNotAdmin} onClick={resetHandlers} className={styles.provisioningButton} />
|
||||
</div>
|
||||
</div>
|
||||
<div hidden={template.length < 1}>
|
||||
<TextField label="Template" value={template} multiline rows={20} />
|
||||
<div className={styles.topMargin}>
|
||||
<PrimaryButton text="Download" onClick={downloadTemplate} className={styles.provisioningButton} />
|
||||
<DefaultButton text="Back" onClick={() => setTemplate("")} className={styles.provisioningButton} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GetProvisioningTemplate;
|
|
@ -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<HTMLElement>, isChecked: boolean) => void;
|
||||
values: IHandlers;
|
||||
disabled:boolean;
|
||||
}
|
||||
|
||||
const Handlers: React.FC<IHandlerProps> = (props) => {
|
||||
const { values } = props;
|
||||
const allHandlers = Object.keys(values);
|
||||
const checkboxes = allHandlers.map((value) =>
|
||||
<Checkbox disabled={props.disabled} key={value} className={styles.handlerCheckbox} checked={values.All || values[value]} name={value} label={value} onChange={props.onHandlerChange} />
|
||||
);
|
||||
return (
|
||||
<div className="ms-Grid">
|
||||
<div className={styles.row}>
|
||||
<div className={styles.column}>
|
||||
{
|
||||
checkboxes.slice(0, 15)
|
||||
}
|
||||
</div>
|
||||
<div className={styles.column}>
|
||||
{
|
||||
checkboxes.slice(15, 30)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Handlers;
|
|
@ -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<ILoadingProps> = (props) => {
|
||||
return (
|
||||
<div hidden={props.hidden}>
|
||||
<h3>
|
||||
Please wait, this process takes a while...
|
||||
</h3>
|
||||
<img src={loadingImage} className={styles.loadingImage} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
|
@ -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"
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 1.4 KiB |
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|