New sample - Sites selected admin

Webpart managing AAD apps with Sites.Selected. Add/Remove sites to the AAD app via an interface in SharePoint
This commit is contained in:
Fredrik Thorild 2021-02-20 08:22:18 +01:00
parent 181f815aa7
commit f4239b5a3e
33 changed files with 18292 additions and 0 deletions

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.11.0",
"libraryName": "site-selected-mngr-wp",
"libraryId": "7fec6393-3d66-4b11-8d55-5f609edf2a7a",
"packageManager": "npm",
"isDomainIsolated": true,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,79 @@
# Sites Selected Admin client-side web part
## Summary
This is a sample SharePoint Framework client-side web part built using react.
The webpart lets you manage your Azure AD applications that have the Sites Selected Api permission. With this web part you'll get an UI for managing what app can connect to which site.
The webpart uses the built in MSGraphClient and needs to be approved in API management. The app asks for Mirosoft Graph Application.Read.All and Sites.FullControl.All. Users of the webpart will need to have Site Collection Administrator privileges to the sites being added to an app.
## Webpart usage
![alt text][Webpart in action]
[Webpart in action]: ./assets/sites-manager-demo.gif "Sites Selected Manager in action"
## Using the webpart to grant an app access to a site, start to finnish
![alt text][Webpart in action - Visual Studio]
[Webpart in action - Visual Studio]: ./assets/vsDemo.gif "Sites Selected Manager Demo"
## Used SharePoint Framework Version
![version](https://img.shields.io/badge/version-1.11-green.svg)
## Applies to
- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
## Prerequisites
> One (or more) Azure AD app with Sites.Selected and the possibility to approve requests in API management (SharePoint Administrator). Site collection administrator is needed for the site(s) you want to give app access to.
## Solution
Solution|Author(s)
--------|---------
react-sites-selected-admin | Fredrik Thorild, https://twitter.com/fthorild
## Version history
Version|Date|Comments
-------|----|--------
1.0|February 19, 2021|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Add an app in Azure AD, or for an exising app add the Sites.Selected Microsoft Graph api permission
![alt text](./assets/aad-appreg.png "AAD app reg")
- Clone this repository
- Ensure that you are at the solution folder
- in the command-line run:
- **gulp bundle --ship**
- **gulp package-solution --ship**
- Add the .sppkg package to your app catalog
- Approve the api access requests
![alt text](./assets/api-access-page.png "API Management")
- Install webpart on a site of your choice
- Add permissions to your app
- Try out the AAD app by sending a request using your favourite method
## References
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-web parts/samples/react-content-query-online" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"sites-selected-manager-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/sitesSelectedManager/SitesSelectedManagerWebPart.js",
"manifest": "./src/webparts/sitesSelectedManager/SitesSelectedManagerWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"SitesSelectedManagerWebPartStrings": "lib/webparts/sitesSelectedManager/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": "site-selected-mngr-wp",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,30 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "site-selected-mngr-wp-client-side-solution",
"id": "7fec6393-3d66-4b11-8d55-5f609edf2a7a",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": true,
"developer": {
"name": "Fredrik Thorild",
"websiteUrl": "https://twitter.com/taxonomythorild",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": ""
},
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "Application.Read.All"
},
{
"resource": "Microsoft Graph",
"scope": "Sites.FullControl.All"
}
]
},
"paths": {
"zippedPackage": "solution/site-selected-mngr-wp.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 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(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
{
"name": "site-selected-mngr-wp",
"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": {
"@fluentui/react": "^7.160.1",
"@microsoft/sp-core-library": "1.11.0",
"@microsoft/sp-lodash-subset": "1.11.0",
"@microsoft/sp-office-ui-fabric-core": "1.11.0",
"@microsoft/sp-property-pane": "1.11.0",
"@microsoft/sp-webpart-base": "1.11.0",
"office-ui-fabric-react": "6.214.0",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"devDependencies": {
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@microsoft/sp-build-web": "1.11.0",
"@microsoft/sp-tslint-rules": "1.11.0",
"@microsoft/sp-module-interfaces": "1.11.0",
"@microsoft/sp-webpart-workbench": "1.11.0",
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2",
"@types/webpack-env": "1.13.1",
"@types/es6-promise": "0.0.33"
}
}

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,36 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "a4411651-9a37-4956-8948-ec5b053da96e",
"alias": "SitesSelectedManagerWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": [
"SharePointWebPart"
],
"preconfiguredEntries": [
{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": {
"default": "Other"
},
"title": {
"default": "Sites Selected Manager"
},
"description": {
"default": "Webpart for managing Azure AD Apps granting site specific access"
},
"officeFabricIconFontName": "AzureLogo",
"properties": {
"description": "Sites Selected Manager",
"showAbout": true,
"aadGuid":"883ea226-0bf2-4a8f-9f9d-92c9162a727d"
}
}
]
}

View File

@ -0,0 +1,70 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneToggle
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'SitesSelectedManagerWebPartStrings';
import SitesSelectedManager from './components/SitesSelectedManager';
import { ISitesSelectedManagerProps } from './components/ISitesSelectedManagerProps';
export interface ISitesSelectedManagerWebPartProps {
description: string;
showAbout: boolean;
aadGuid: string;
}
export default class SitesSelectedManagerWebPart extends BaseClientSideWebPart<ISitesSelectedManagerWebPartProps> {
public render(): void {
const element: React.ReactElement<ISitesSelectedManagerProps> = React.createElement(
SitesSelectedManager,
{
description: this.properties.description,
context: this.context,
showAbout: this.properties.showAbout,
aadGuid: this.properties.aadGuid
}
);
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: [
{
groups: [
{
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
}),
PropertyPaneToggle('showAbout',
{
label: strings.ShowAboutFieldLabel,
checked: this.properties.showAbout === true,
}),
PropertyPaneTextField('aadGuid', {
label: strings.AADGuidLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,74 @@
import { ICommandBarItemProps } from "office-ui-fabric-react";
import { ISitesSelectedManagerProps } from "./ISitesSelectedManagerProps";
export interface IDialogProps {
isHidden: boolean;
hideDialog: (hide: boolean) => void;
webPartProperties: ISitesSelectedManagerProps,
selectedApp: string;
isDeleteMode: boolean;
}
export interface IAppListItem {
key: number;
name: string;
value: string;
}
export interface IAppListState {
items?: IAppListItem[];
selectionDetails?: string;
menuItems?: ICommandBarItemProps[];
dialogHidden?: boolean,
isDeleteMode?: boolean;
}
export interface IMessageBoxProps {
resetChoice?: () => void;
}
export interface ISitePermissionList {
value: ISitesSelectedPermissionPayload[];
}
export interface ISitesSelectedPermissionPayload {
roles?: string[];
grantedToIdentities?: IAADApplicationWrapper[];
id?: string;
}
export interface ISelectedSitesListProps {
value: Array<IAADApplication>;
webpartProperties: ISitesSelectedManagerProps;
}
export interface ISPSite {
displayName: string;
id: string;
}
export interface IAADApplicationList {
value: Array<IAADApplication>;
}
export interface IAADApplication {
id: string;
appId?: string;
displayName: string;
requiredResourceAccess?: Array<IRequiredResourceAccess>;
}
export interface IAADApplicationWrapper {
application: IAADApplication;
}
export interface IRequiredResourceAccess {
resourceAppId: string;
resourceAccess: Array<IResourceAccess>;
}
export interface IResourceAccess {
id: string;
type: string;
}

View File

@ -0,0 +1,8 @@
import { WebPartContext } from '@microsoft/sp-webpart-base';
export interface ISitesSelectedManagerProps {
description: string;
context: WebPartContext;
showAbout: boolean;
aadGuid: string;
}

View File

@ -0,0 +1,168 @@
import * as React from 'react';
import { IAADApplication, IAADApplicationList, IMessageBoxProps, ISPSite } from './ISitesSelectedAppInterfaces';
import { ISitesSelectedManagerProps } from './ISitesSelectedManagerProps';
import { SitesSelectedAppList } from './SitesSelectedList';
import { Icon, MessageBar, MessageBarType, Pivot, PivotItem, PrimaryButton, Stack, TextField } from 'office-ui-fabric-react';
import { Spinner } from '@fluentui/react';
import styles from './SitesSelectedManager.module.scss';
export const SitesSelectedApp: React.FunctionComponent<ISitesSelectedManagerProps> = (props) => {
const [appState, setAppState] = React.useState<Array<IAADApplication>>()
const [site, setSite] = React.useState("");
const [permString, setpermString] = React.useState("");
const [showMessage, setShowMessage] = React.useState(false);
const [messageBarType, setMessageBarType] = React.useState<MessageBarType>();
const [message, setMessage] = React.useState("");
React.useEffect(() => {
const fetchData = async () => {
setShowMessage(false);
try {
const client = await props.context.msGraphClientFactory.getClient();
const aadApps: IAADApplicationList = await client
.api('applications')
.version("v1.0")
.select("id,appId,displayName,requiredResourceAccess")
.get();
const appsWithSitesSelected = aadApps.value.filter((obj) => {
return obj.requiredResourceAccess.some(({ resourceAccess }) =>
resourceAccess.some(({ id }) => id === props.aadGuid))
});
setAppState(appsWithSitesSelected);
if (appsWithSitesSelected.length === 0) {
setMessageBarType(MessageBarType.info)
setMessage(`We couldn't find any apps with [Sites.Selected] - Don't think that's right?
Then you might want to double check the guid in webpart settings - The default is 883ea226-0bf2-4a8f-9f9d-92c9162a727d`);
setShowMessage(true);
}
} catch (error) {
setMessageBarType(MessageBarType.error)
if (error.statusCode) {
setMessage(`Http error occured ${error.statusCode} - ${error.message} - have you consented this web part in API management?`);
} else {
setMessage(`Unknown error occured getting your apps - have you consented this web part in API management?`);
}
setAppState([]);
setShowMessage(true);
}
}
fetchData()
}, [])
const checkSitePermission = async () => {
setpermString("...loading - Getting site");
setShowMessage(false);
try {
const url = new URL(site);
const client = await props.context.msGraphClientFactory.getClient();
const siteData: ISPSite = await client
.api(`sites/${url.hostname}:${url.pathname}`)
.version("v1.0")
.select("displayName,id,description")
.get();
setpermString("...loading - Got the site");
const perms = await client
.api(`sites/${siteData.id}/permissions`)
.version("v1.0")
.get()
setpermString(JSON.stringify(perms.value, undefined, 4))
} catch (error) {
setMessageBarType(MessageBarType.error)
if (error.statusCode) {
setMessage(`Http error occured ${error.statusCode} - ${error.message} - Check the format of your URL
Correct format below:
https://tenant.sharepoint.com/sites/thesite`);
} else {
setMessage(`Unknown error`);
}
setpermString("");
setShowMessage(true);
}
}
const SitesSelectedMessageBox = (p: IMessageBoxProps) => (
<MessageBar
messageBarType={messageBarType}
isMultiline={true}
onDismiss={p.resetChoice}
dismissButtonAriaLabel="Close"
>
{message}
</MessageBar>
);
const resetChoice = React.useCallback(() => setShowMessage(false), []);
if (appState) {
return <div>
<h1>{props.description}</h1>
<Pivot>
{props.showAbout ? <PivotItem
headerText="Home / About"
headerButtonProps={{
'data-order': 1,
'data-title': 'Home / About'
}}
itemIcon="Home"
>
<h3>What can this webpart do?</h3>
<ul>
<li><Icon iconName="SharepointAppIcon16" /> List Azure AD applications that have the Microsoft graph api scope [Sites.Selected]</li>
<li><Icon iconName="SharepointAppIcon16" /> Add SharePoint sites to the listed apps which will enable the app to interact with these sites via the graph api</li>
<li><Icon iconName="SharepointAppIcon16" /> Clear all SharePoint site permissions for the selected app</li>
<li><Icon iconName="Permissions" /> Check what app(s) that has been added to a specific SharePoint site</li>
</ul>
<h3>Good to know</h3>
<p>
Due to api- and other limitations it is "not possible" to list all sites that have an app with permissions via this concept.
Furthermore, when checking a site you will see that it has n apps with access but not what access (Read,Write or Read/Write)
</p>
<h3>User access</h3>
<p>
In order to grant access for an app, the user of this webpart has to be a Site Collection Administrator of the site.
</p>
</PivotItem> : React.Fragment}
<PivotItem headerText="Add/Remove sites to Apps" itemIcon="SharepointAppIcon16">
<SitesSelectedAppList {...{ value: appState, webpartProperties: props }} />
{showMessage ? <SitesSelectedMessageBox resetChoice={resetChoice} /> : React.Fragment}
</PivotItem>
<PivotItem headerText="Check app permissions on a site" itemIcon="Permissions">
<h3>Use the form below to check a sites permissions</h3>
<p><strong>Info!</strong> If the result box shows [] it means there is no permissions granted</p>
<Stack className={styles.checkPermUi}>
<TextField onChange={(e: any) => setSite(e.target.value)} label="SharePoint site"
placeholder="Please enter URL here" />
<PrimaryButton text="Check permission" onClick={checkSitePermission} allowDisabledFocus />
<TextField value={permString} label="(Raw) - Permission object for site" multiline autoAdjustHeight />
</Stack>
{showMessage ? <SitesSelectedMessageBox resetChoice={resetChoice} /> : React.Fragment}
</PivotItem>
</Pivot>
</div>
}
else {
return <div>
<Spinner label="Working on it..." />
</div>
}
}

View File

@ -0,0 +1,213 @@
import * as React from 'react';
import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { ChoiceGroup, IChoiceGroupOption } from 'office-ui-fabric-react/lib/ChoiceGroup';
import { MessageBar, MessageBarType, TextField } from 'office-ui-fabric-react';
import { ISitesSelectedManagerProps } from './ISitesSelectedManagerProps';
import styles from './SitesSelectedManager.module.scss';
import { IAADApplicationWrapper, IDialogProps, IMessageBoxProps, ISitePermissionList, ISitesSelectedPermissionPayload, ISPSite } from './ISitesSelectedAppInterfaces';
const options = [
{
key: 'read',
text: 'Read',
},
{
key: 'write',
text: 'Write',
},
{
key: 'read-write',
text: 'Read / Write',
},
];
const modelProps = {
isBlocking: false,
styles: { main: { maxWidth: 450 } },
};
const addDialogContentProps = {
type: DialogType.largeHeader,
title: 'Grant access to the selected app to a SharePoint site collection',
subText: 'Enter a SharePoint site collection URL into the text field and select the wanted access level',
};
const deleteDialogContentProps = {
type: DialogType.largeHeader,
title: 'Remove the access for the selected app to a SharePoint site collection',
subText: 'Enter a SharePoint site collection URL into the text field and click "remove" to remove the access',
};
export const SitesSelectedDialog: React.FunctionComponent<IDialogProps> = (props) => {
const [site, setSite] = React.useState("");
const [perm, setPerm] = React.useState("");
const [showMessage, setShowMessage] = React.useState(false);
const [messageBarType, setMessageBarType] = React.useState<MessageBarType>();
const [message, setMessage] = React.useState("");
const _addPermissionToSite = async () => {
setShowMessage(false);
try {
const url = new URL(site);
const client = await props.webPartProperties.context.msGraphClientFactory.getClient();
const siteData: ISPSite = await client
.api(`sites/${url.hostname}:${url.pathname}`)
.version("v1.0")
.select("displayName,id,description")
.get()
const app: IAADApplicationWrapper = { application: { displayName: props.selectedApp.split('|')[0], id: props.selectedApp.split('|')[1] } }
const pl: ISitesSelectedPermissionPayload = {
roles: perm.split('-'),
grantedToIdentities: [app]
}
await client
.api(`sites/${siteData.id}/permissions`)
.version("v1.0")
.post(pl)
_handleSuccess("Yay! - Permissions successfully added!");
} catch (error) {
_handleError(error)
}
}
const _deletePermissionToSite = async () => {
try {
setShowMessage(false);
const url = new URL(site);
const client = await props.webPartProperties.context.msGraphClientFactory.getClient();
const siteData: ISPSite = await client
.api(`sites/${url.hostname}:${url.pathname}`)
.version("v1.0")
.select("displayName,id,description")
.get()
const permList: ISitePermissionList = await client
.api(`sites/${siteData.id}/permissions`)
.version("v1.0")
.get()
const permissionIdToRemove = _getPermissionIdFromPayload(props.selectedApp.split('|')[1], permList);
if (permissionIdToRemove) {
await client
.api(`sites/${siteData.id}/permissions/${permissionIdToRemove}`)
.version("v1.0")
.delete()
}
_handleSuccess("Yay! - Permissions successfully deleted!");
} catch (error) {
_handleError(error);
}
}
const _handleError = (error: any) => {
setMessageBarType(MessageBarType.error)
if (error.statusCode) {
setMessage(`Http error occured ${error.statusCode} - ${error.message} - Check the format of your URL
Correct format below:
https://tenant.sharepoint.com/sites/thesite`);
} else {
setMessage(`Unknown error occured`);
}
setShowMessage(true);
props.hideDialog(true);
}
const _handleSuccess = (message: string) => {
setMessageBarType(MessageBarType.success)
setMessage(message);
setShowMessage(true);
props.hideDialog(true);
setTimeout(() => {
setShowMessage(false);
}, 5000);
}
const _getPermissionIdFromPayload = (appId: string, payload: ISitePermissionList): string => {
let result: string;
payload.value.forEach(element => {
element.grantedToIdentities.forEach(el => {
console.warn(el.application.id === appId);
if (el.application.id === appId)
result = element.id
})
});
if (!result) {
throw new Error("App could not be found for site");
} else {
return result;
}
}
const _onChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
setPerm(option.key);
}
const SitesSelectedStatusMessage = (p: IMessageBoxProps) => (
<MessageBar
messageBarType={messageBarType}
isMultiline={true}
onDismiss={p.resetChoice}
dismissButtonAriaLabel="Close"
>
{message}
</MessageBar>
);
const resetChoice = React.useCallback(() => setShowMessage(false), []);
if (props.isDeleteMode) {
return (
<>
{showMessage ? <SitesSelectedStatusMessage resetChoice={resetChoice} /> : React.Fragment}
<Dialog className={styles.sitesSelectedManager}
hidden={props.isHidden}
onDismiss={(() => { props.hideDialog(true) })}
dialogContentProps={deleteDialogContentProps}
modalProps={modelProps}
>
<TextField onChange={(e: any) => setSite(e.target.value)} label="SharePoint site"
placeholder="Please enter URL here" />
<DialogFooter>
<PrimaryButton onClick={_deletePermissionToSite} text="Save" />
<DefaultButton onClick={(() => props.hideDialog(true))} text="Cancel" />
</DialogFooter>
</Dialog>
</>
);
}
else {
return (
<>
{showMessage ? <SitesSelectedStatusMessage resetChoice={resetChoice} /> : React.Fragment}
<Dialog className={styles.sitesSelectedManager}
hidden={props.isHidden}
onDismiss={(() => { props.hideDialog(true) })}
dialogContentProps={addDialogContentProps}
modalProps={modelProps}
>
<TextField onChange={(e: any) => setSite(e.target.value)} label="SharePoint site"
placeholder="Please enter URL here" />
<ChoiceGroup onChange={_onChange} options={options} />
<DialogFooter>
<PrimaryButton onClick={_addPermissionToSite} text="Save" />
<DefaultButton onClick={(() => props.hideDialog(true))} text="Cancel" />
</DialogFooter>
</Dialog>
</>
);
}
};

View File

@ -0,0 +1,142 @@
import * as React from 'react';
import { DetailsList, DetailsListLayoutMode, Selection, IColumn, CheckboxVisibility, SelectionMode } from 'office-ui-fabric-react/lib/DetailsList';
import { MarqueeSelection } from 'office-ui-fabric-react/lib/MarqueeSelection';
import { Fabric } from 'office-ui-fabric-react/lib/Fabric';
import { IAppListItem, IAppListState, ISelectedSitesListProps } from './ISitesSelectedAppInterfaces';
import { ICommandBarItemProps, CommandBar, IButtonProps } from 'office-ui-fabric-react';
import styles from './SitesSelectedManager.module.scss';
import { SitesSelectedDialog } from './SitesSelectedDialog';
export class SitesSelectedAppList extends React.Component<ISelectedSitesListProps, IAppListState> {
private _selection: Selection;
private _allItems: IAppListItem[];
private _columns: IColumn[];
private _items: ICommandBarItemProps[];
private _overflowButtonProps: IButtonProps;
constructor(props) {
super(props);
this._hideDialog = this._hideDialog.bind(this);
this._items = this._getMenu(true);
this._selection = new Selection({
onSelectionChanged: () => this.setState({ selectionDetails: this._getSelectionDetails() }),
});
this._allItems = [];
let i = 0;
this.props.value.forEach(element => {
this._allItems.push({
key: i,
name: element.displayName,
value: element.appId,
});
i = i + 1;
});
this._columns = [
{ key: 'column1', name: 'App Name', fieldName: 'name', minWidth: 100, maxWidth: 200, isResizable: true },
{ key: 'column2', name: 'Azure AD App Id', fieldName: 'value', minWidth: 100, maxWidth: 200, isResizable: true },
];
this.state = {
items: this._allItems,
selectionDetails: this._getSelectionDetails(),
menuItems: this._items,
dialogHidden: true
};
}
private _hideDialog(hide: boolean) {
this.setState(
{ dialogHidden: hide }
)
}
private _getSelectionDetails(): string {
const selectionCount = this._selection.getSelectedCount();
switch (selectionCount) {
case 0:
this.setState(
{
menuItems: this._getMenu(true)
}
)
return 'No items selected';
case 1:
this.setState(
{
menuItems: this._getMenu(false)
}
)
const result = this._selection.getSelection()[0] as IAppListItem;
return `${result.name}|${result.value}`;
default:
return `${selectionCount} items selected`;
}
}
private _getMenu(disabled: boolean): ICommandBarItemProps[] {
return [
{
key: 'newItem',
text: 'Add app permissions',
iconProps: { iconName: 'CloudAdd' },
split: false,
ariaLabel: 'New',
onClick: () => { this.setState({ dialogHidden: false, isDeleteMode: false }) },
disabled: disabled,
},
{
key: 'upload',
text: 'Clear app permissions',
iconProps: { iconName: 'BlockedSiteSolid12' },
split: false,
onClick: () => { this.setState({ dialogHidden: false, isDeleteMode: true }) },
disabled: disabled,
}
];
}
public render(): JSX.Element {
const { items } = this.state;
return (
<Fabric>
<div>
<CommandBar className={styles.commandBar}
items={this.state.menuItems}
overflowButtonProps={this._overflowButtonProps}
ariaLabel="Use left and right arrow keys to navigate between commands"
/>
</div>
<MarqueeSelection className={styles.listMargin} selection={this._selection}>
<DetailsList
items={items}
columns={this._columns}
setKey="set"
layoutMode={DetailsListLayoutMode.justified}
selection={this._selection}
checkboxVisibility={CheckboxVisibility.onHover}
selectionMode={SelectionMode.single}
selectionPreservedOnEmptyClick={true}
ariaLabelForSelectionColumn="Toggle selection"
ariaLabelForSelectAllCheckbox="Toggle selection for all items"
checkButtonAriaLabel="Row checkbox"
/>
</MarqueeSelection>
<div>&nbsp;</div>
<SitesSelectedDialog {...
{
isHidden: this.state.dialogHidden,
hideDialog: this._hideDialog,
webPartProperties: this.props.webpartProperties,
selectedApp: this.state.selectionDetails,
isDeleteMode: this.state.isDeleteMode
}} />
</Fabric >
);
}
}

View File

@ -0,0 +1,104 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.listMargin{
margin-bottom: 50px;
}
.commandBar {
margin-top: 20px;
button{
border: 0px solid white!important;
}
}
.checkPermUi{
button{
margin-top: 20px!important;
margin-bottom: 20px!important;
width: 155px;
}
}
.sitesSelectedManager {
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
.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);
}
footer{
height: 80px;
}
label{
font-weight: 600;
font-size: small;
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,15 @@
import * as React from 'react';
import styles from './SitesSelectedManager.module.scss';
import { ISitesSelectedManagerProps } from './ISitesSelectedManagerProps';
import { SitesSelectedApp } from './SitesSelectedApp';
export default class SitesSelectedManager extends React.Component<ISitesSelectedManagerProps, {}> {
public render(): React.ReactElement<ISitesSelectedManagerProps> {
return (
<div className={styles.sitesSelectedManager}>
<SitesSelectedApp {...this.props} />
<footer />
</div>
);
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"DescriptionFieldLabel": "Webpart Title",
"ShowAboutFieldLabel":"Show Home / About Tab",
"AADGuidLabel":"Sites Selected Permission GUID"
}
});

View File

@ -0,0 +1,10 @@
declare interface ISitesSelectedManagerWebPartStrings {
DescriptionFieldLabel: string;
ShowAboutFieldLabel: string;
AADGuidLabel:string;
}
declare module 'SitesSelectedManagerWebPartStrings' {
const strings: ISitesSelectedManagerWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

View File

@ -0,0 +1,39 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/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",
"src/**/*.tsx"
],
"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
}
}