Merge pull request #966 from LauraKokkarinen/PR951

Manual merge of PR951
This commit is contained in:
Laura Kokkarinen 2019-08-26 19:38:22 +03:00 committed by GitHub
commit 0772cf6637
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1673 additions and 0 deletions

View File

@ -0,0 +1,2 @@
.vscode
/webpart/package-lock.json

View File

@ -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" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

View File

@ -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

View 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

View File

@ -0,0 +1 @@
Copy your pfx file here before publish the function.

View File

@ -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";
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -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 -->"
}

View File

@ -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"
}
}

View File

@ -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/"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -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);

View File

@ -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"
}
}

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -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;

View File

@ -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,
);
}
}

View File

@ -0,0 +1,4 @@
const headers : Headers= new Headers();
headers.append("Accept","application/json");
headers.append("Content-type","application/json");
export default headers;

View File

@ -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": ""
}
}]
}

View File

@ -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
})
]
}
]
}
]
};
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,6 @@
import AppService from "../../../../services/appService";
export interface IAppProps {
appService: AppService;
webUrl:string;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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"
}
});

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -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"
]
}

View File

@ -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
}
}