Switch to functional components and re-factor

Code clean up, no chnages to the UI.
This commit is contained in:
Fredrik Thorild 2021-03-08 18:49:33 +01:00
parent 71c62ef07d
commit a62ac9e0e0
19 changed files with 749 additions and 645 deletions

View File

@ -50,6 +50,7 @@ react-sites-selected-admin | Fredrik Thorild [@fthorild](https://twitter.com/fth
Version|Date|Comments Version|Date|Comments
-------|----|-------- -------|----|--------
1.0|February 19, 2021|Initial release 1.0|February 19, 2021|Initial release
1.1|March 8, 2021|Switch to functional components. Re-factor
## Disclaimer ## Disclaimer

View File

@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "site-selected-mngr-wp-client-side-solution", "name": "site-selected-mngr-wp-client-side-solution",
"id": "7fec6393-3d66-4b11-8d55-5f609edf2a7a", "id": "7fec6393-3d66-4b11-8d55-5f609edf2a7a",
"version": "1.0.0.0", "version": "1.1.0.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"isDomainIsolated": true, "isDomainIsolated": true,
"developer": { "developer": {

View File

@ -0,0 +1,163 @@
import { ServiceKey, ServiceScope } from '@microsoft/sp-core-library';
import { MSGraphClientFactory, MSGraphClient } from '@microsoft/sp-http';
import { IAzureApp, IPermission } from './components/IAppInterfaces';
import * as strings from 'SitesSelectedManagerWebPartStrings';
export interface IService {
getApps(apiPermission: string): Promise<IAzureApp[]>;
getPermissions(siteUrl: URL): Promise<IPermission[]>
addPermissions(siteUrl: URL, payload: IPermission): Promise<void>;
deletePermissions(siteUrl: URL, appId: string): Promise<void>;
}
export class Service implements IService {
public static readonly serviceKey: ServiceKey<IService> =
ServiceKey.create<IService>('sites-selected-admin-app:IService', Service);
private _msGraphClientFactory: MSGraphClientFactory;
private _siteId: string;
public get siteId(): string {
return this._siteId;
}
public set siteId(v: string) {
this._siteId = v;
}
constructor(serviceScope: ServiceScope) {
serviceScope.whenFinished(() => {
this._msGraphClientFactory = serviceScope.consume(MSGraphClientFactory.serviceKey);
});
}
private getSiteId(siteUrl: URL): Promise<void> {
return new Promise<void>(async (resolve, reject) => {
const client = await this._msGraphClientFactory.getClient();
client
.api(`sites/${siteUrl.hostname}:${siteUrl.pathname}`)
.version("v1.0")
.select("id")
.get((error, site: any, rawResponse?: any) => {
if (site) {
this.siteId = site.id;
resolve()
} else {
error ? reject(error) : reject(strings.ErrorUnknown)
}
})
});
}
private getPermissionId(appId: string, permssions: IPermission[]): string {
let result: string;
permssions.forEach(element => {
element.grantedToIdentities.forEach(el => {
if (el.application.id === appId)
result = element.id
})
});
if (!result) {
return null;
} else {
return result;
}
}
public getApps(apiPermissionGuid: string): Promise<IAzureApp[]> {
return new Promise<IAzureApp[]>(async (resolve, reject) => {
const client = await this._msGraphClientFactory.getClient();
client
.api('applications')
.version("v1.0")
.select("id,appId,displayName,requiredResourceAccess")
.get((error, apps: any, rawResponse?: any) => {
if (apps) {
const appsWithSitesSelected = apps.value.filter((obj) => {
return obj.requiredResourceAccess.some(({ resourceAccess }) =>
resourceAccess.some(({ id }) => id === apiPermissionGuid))
});
resolve(appsWithSitesSelected as IAzureApp[]);
} else {
error ? reject(error) : reject(strings.ErrorUnknown)
}
});
});
}
public getPermissions(siteUrl: URL): Promise<IPermission[]> {
return new Promise<IPermission[]>(async (resolve, reject) => {
const client = await this._msGraphClientFactory.getClient();
await this.getSiteId(siteUrl);
client
.api(`sites/${this.siteId}/permissions`)
.version("v1.0")
.get((error, permissions: any, rawResponse?: any) => {
if (permissions) {
resolve(permissions.value);
} else {
error ? reject(error) : reject(strings.ErrorUnknown)
}
});
});
}
public addPermissions(siteUrl: URL, payload: IPermission): Promise<void> {
return new Promise<void>(async (resolve, reject) => {
const client = await this._msGraphClientFactory.getClient();
await this.getSiteId(siteUrl);
client
.api(`sites/${this.siteId}/permissions`)
.version("v1.0")
.post(payload, (error, response: any, rawResponse?: any) => {
if (error) {
error ? reject(error) : reject(strings.ErrorUnknown)
} else {
resolve();
}
});
});
}
public deletePermissions(siteUrl: URL, appId: string): Promise<void> {
return new Promise<void>(async (resolve, reject) => {
console.warn('Are we herrrrre');
const client = await this._msGraphClientFactory.getClient();
console.warn('Are we herrrrre1');
const permissions = await this.getPermissions(siteUrl);
console.warn('Are we herrrrre2');
console.warn('ahhhahha');
console.warn(appId);
const permissionId = this.getPermissionId(appId, permissions);
console.warn('Are we herrrrre3....');
console.warn(permissionId);
if (permissionId) {
client
.api(`sites/${this.siteId}/permissions/${permissionId}`)
.version("v1.0")
.delete((error, response: any, rawResponse?: any) => {
if (error) {
console.warn('asdasd');
error ? reject(error) : reject(strings.ErrorUnknown)
} else {
console.warn('ååååååååååååååå');
resolve();
}
});
} else {
reject(`${strings.ErrorNoPermissionsFound} ${appId}`)
}
});
}
}

View File

@ -3,19 +3,15 @@
"id": "a4411651-9a37-4956-8948-ec5b053da96e", "id": "a4411651-9a37-4956-8948-ec5b053da96e",
"alias": "SitesSelectedManagerWebPart", "alias": "SitesSelectedManagerWebPart",
"componentType": "WebPart", "componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*", "version": "*",
"manifestVersion": 2, "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, "requiresCustomScript": false,
"supportedHosts": [ "supportedHosts": [
"SharePointWebPart" "SharePointWebPart"
], ],
"preconfiguredEntries": [ "preconfiguredEntries": [
{ {
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other "groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": { "group": {
"default": "Other" "default": "Other"
}, },

View File

@ -1,29 +1,25 @@
import * as React from 'react'; import * as React from 'react';
import * as ReactDom from 'react-dom'; import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import { import {
IPropertyPaneConfiguration, IPropertyPaneConfiguration,
PropertyPaneTextField, PropertyPaneTextField,
PropertyPaneToggle PropertyPaneToggle
} from '@microsoft/sp-property-pane'; } from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base'; import { BaseClientSideWebPart, WebPartContext } from '@microsoft/sp-webpart-base';
import * as strings from 'SitesSelectedManagerWebPartStrings'; import * as strings from 'SitesSelectedManagerWebPartStrings';
import SitesSelectedManager from './components/SitesSelectedManager'; import { App } from './components/App';
import { ISitesSelectedManagerProps } from './components/ISitesSelectedManagerProps';
export interface ISitesSelectedManagerWebPartProps { export interface IAppProperties {
description: string; description: string;
context: WebPartContext;
showAbout: boolean; showAbout: boolean;
aadGuid: string; aadGuid: string;
} }
export default class SitesSelectedManagerWebPart extends BaseClientSideWebPart<ISitesSelectedManagerWebPartProps> { export default class SitesSelectedManagerWebPart extends BaseClientSideWebPart<IAppProperties> {
public render(): void { public render(): void {
const element: React.ReactElement<ISitesSelectedManagerProps> = React.createElement( const element: React.ReactElement<IAppProperties> = React.createElement(
SitesSelectedManager, App,
{ {
description: this.properties.description, description: this.properties.description,
context: this.context, context: this.context,
@ -38,10 +34,6 @@ export default class SitesSelectedManagerWebPart extends BaseClientSideWebPart<I
ReactDom.unmountComponentAtNode(this.domElement); ReactDom.unmountComponentAtNode(this.domElement);
} }
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return { return {
pages: [ pages: [

View File

@ -0,0 +1,131 @@
import * as React from 'react';
import { IAzureApp } from './IAppInterfaces';
import { AppList } from './AppList';
import { Icon, MessageBar, MessageBarType, Pivot, PivotItem } from 'office-ui-fabric-react';
import { Spinner } from '@fluentui/react';
import { IService, Service } from '../Service';
import { IAppProperties } from '../SitesSelectedManagerWebPart';
import * as strings from 'SitesSelectedManagerWebPartStrings';
import { AppCheckPermissions } from './AppCheckPermissions';
interface IMessageBoxProps {
resetChoice?: () => void;
}
interface IAppState {
showMessage: boolean;
site: string;
messageBarType: number;
message: string;
permissionJson: string;
apps: IAzureApp[];
getPerm: boolean;
permission: string;
}
export const App: React.FunctionComponent<IAppProperties> = (props) => {
const [state, setState] = React.useState<IAppState>(
{ showMessage: false, site: '', messageBarType: null, message: '', permissionJson: '', apps: null, getPerm: false, permission: '' }
);
const [service] = React.useState<IService>(props.context.serviceScope.consume(Service.serviceKey))
React.useEffect(() => {
setState({ ...state, showMessage: false });
service.getApps(props.aadGuid)
.then((_apps) => {
setState({ ...state, apps: _apps });
if (_apps.length === 0) {
setState({
...state,
message: strings.ErrorNoAppsFoundMessage,
messageBarType: MessageBarType.info,
showMessage: true
});
}
},
(error) => {
let _errorDetail = strings.ErrorGettingApps;
const _errorHint = strings.ErrorHintGettingApps;
if (error.statusCode) {
_errorDetail = `${strings.ErrorHttp} ${error.statusCode} - ${error.message}`
}
setState({
...state,
messageBarType: MessageBarType.error,
apps: [],
message: `${_errorDetail} ${_errorHint}`,
showMessage: true
});
})
}, [])
const showMessage = (type: MessageBarType, message: string, autoDismiss: boolean = false, error?: any): void => {
setState({ ...state, showMessage: true, messageBarType: type, message: message });
if (autoDismiss) {
setTimeout(() => {
setState({ ...state, showMessage: false });
}, 5000);
}
}
const resetChoice = () => {
setState({ ...state, showMessage: false, message: '' });
};
const SitesSelectedMessageBox = (p: IMessageBoxProps) => (
<MessageBar
messageBarType={state.messageBarType}
isMultiline={true}
onDismiss={resetChoice}
dismissButtonAriaLabel={strings.Close}
>
{state.message}
</MessageBar>
);
if (state.apps) {
return <div>
<h1>{props.description}</h1>
{state.showMessage ? <SitesSelectedMessageBox resetChoice={resetChoice} /> : React.Fragment}
<Pivot>
{props.showAbout ? <PivotItem
headerText={strings.HomeTabTitle}
headerButtonProps={{
'data-order': 1,
'data-title': strings.HomeTabTitle
}}
itemIcon="Home"
>
<h3>{strings.HomeTitleMain}</h3>
<ul>
<li><Icon iconName="SharepointAppIcon16" /> {strings.HomeBulletList}</li>
<li><Icon iconName="SharepointAppIcon16" /> {strings.HomeBulletAdd} </li>
<li><Icon iconName="SharepointAppIcon16" /> {strings.HomeBulletClear} </li>
<li><Icon iconName="Permissions" /> {strings.HomeBulletCheck} </li>
</ul>
<h3>{strings.HomeTitleFYI}</h3>
<p>
{strings.HomeFYI}
</p>
<h3>{strings.HomeAccessTitle}</h3>
<p>{strings.HomeAccess}</p>
</PivotItem> : React.Fragment}
<PivotItem headerText={strings.AddTabTitle} itemIcon="SharepointAppIcon16">
<AppList {...{ applications: state.apps, wpContext: props.context, showMessage: showMessage }} />
</PivotItem>
<PivotItem headerText={strings.CheckTabTitle} itemIcon="Permissions">
<AppCheckPermissions {...{ wpContext: props.context, showMessage: showMessage }} />
</PivotItem>
</Pivot>
</div>
}
else {
return <div>
<Spinner label={strings.WorkingOnIt} />
</div>
}
}

View File

@ -0,0 +1,59 @@
import * as React from 'react';
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { MessageBarType, Stack, TextField } from 'office-ui-fabric-react';
import styles from './SitesSelectedManager.module.scss';
import { IService, Service } from '../Service';
import * as strings from 'SitesSelectedManagerWebPartStrings';
import { WebPartContext } from '@microsoft/sp-webpart-base';
interface IAppCheckPermissionsProps {
wpContext: WebPartContext,
showMessage: (type: MessageBarType, message: string, autoDismiss: boolean, error?: any) => void;
}
interface IAppCheckPermissionsState {
getPerm: boolean;
site?: string;
permissionJson: string;
}
export const AppCheckPermissions: React.FunctionComponent<IAppCheckPermissionsProps> = (props) => {
const [state, setState] = React.useState<IAppCheckPermissionsState>({ getPerm: false, permissionJson: '' });
const [service] = React.useState<IService>(props.wpContext.serviceScope.consume(Service.serviceKey))
const toggle = () => setState({ ...state, getPerm: !state.getPerm });
React.useEffect(() => {
if (state.site) {
setState({ ...state, permissionJson: strings.LoadingMessage });
const url = new URL(state.site);
service.getPermissions(url)
.then((permissions) => {
setState({ ...state, permissionJson: JSON.stringify(permissions, undefined, 4) });
},
(error) => {
let errorDetail = strings.ErrorGeneric;
let errorHint = ''
if (error.statusCode) {
errorDetail = strings.ErrorHttp
errorHint = `${error.statusCode} - ${error.message} ${strings.ErrorHintUrlFormat}`
}
props.showMessage(MessageBarType.error, `${errorDetail} ${errorHint}`, false);
})
}
}, [state.getPerm])
return <div><h3>{strings.PermCheckTitle}</h3>
<p><strong>{strings.Info}</strong> {strings.PermCheckHint}</p>
<Stack className={styles.checkPermUi}>
<TextField onChange={(e: any) => setState({ ...state, site: e.target.value })} label={strings.CheckSiteLabel}
placeholder={strings.CheckSitePlaceholder} />
<PrimaryButton text={strings.CheckButtonText} onClick={toggle} allowDisabledFocus />
<TextField value={state.permissionJson} label={strings.CheckTextAreaLabel} multiline autoAdjustHeight />
</Stack>
</div>
}

View File

@ -0,0 +1,113 @@
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 { MessageBarType, TextField } from 'office-ui-fabric-react';
import styles from './SitesSelectedManager.module.scss';
import { IAzureApp, IPermission } from './IAppInterfaces';
import { IService, Service } from '../Service';
import * as strings from 'SitesSelectedManagerWebPartStrings';
import { WebPartContext } from '@microsoft/sp-webpart-base';
interface IAppDialogProps {
isHidden: boolean;
hideDialog: (hide: boolean) => void;
wpContext: WebPartContext,
selectedApp: IAzureApp;
isDeleteMode: boolean;
showMessage: (type: MessageBarType, message: string, autoDismiss: boolean, error?: any) => void;
}
interface IAppDialogState {
site: string;
permission: string;
}
export const AppDialog: React.FunctionComponent<IAppDialogProps> = (props) => {
const [state, setState] = React.useState<IAppDialogState>(
{ site: '', permission: '' });
const [service] = React.useState<IService>(props.wpContext.serviceScope.consume(Service.serviceKey))
const [mode, setMode] = React.useState({ add: false, delete: false });
React.useEffect(() => {
if (state.site) {
const url = new URL(state.site);
const payload: IPermission = {
roles: state.permission.split('-'),
grantedToIdentities: [{ application: props.selectedApp }]
}
console.warn(payload);
service.addPermissions(url, payload).then(() => {
props.showMessage(MessageBarType.success, strings.DialogAddSuccess, true);
}, (error) => {
props.showMessage(MessageBarType.error, strings.ErrorGeneric, false, error);
})
props.hideDialog(true);
}
}, [mode.add])
React.useEffect(() => {
if (state.site) {
service.deletePermissions(new URL(state.site), props.selectedApp.id).then(() => {
props.showMessage(MessageBarType.success, strings.DialogRemoveSuccess, true);
}, (error: any) => {
props.showMessage(MessageBarType.error, strings.ErrorGeneric, false, error);
});
props.hideDialog(true);
}
}, [mode.delete])
const addDialogContentProps = {
type: DialogType.largeHeader,
title: strings.DialogAddTitle,
subText: strings.DialogAddSubTitle,
};
const deleteDialogContentProps = {
type: DialogType.largeHeader,
title: strings.DialogDelTitle,
subText: strings.DialogDelSubTitle,
};
return (
<>
<Dialog className={styles.sitesSelectedManager}
hidden={props.isHidden}
onDismiss={(() => { props.hideDialog(true) })}
dialogContentProps={props.isDeleteMode ? deleteDialogContentProps : addDialogContentProps}
modalProps={{ isBlocking: false, styles: { main: { maxWidth: 450 } }, }}
>
<TextField onChange={(e: any) => setState({ ...state, site: e.target.value })} label={strings.CheckSiteLabel}
placeholder={strings.CheckSitePlaceholder} />
<ChoiceGroup className={props.isDeleteMode ? styles.dialogHidden : styles.dialogShow} onChange={(ev: any, option: IChoiceGroupOption) => {
setState({ ...state, permission: option.key })
}} options={[
{
key: 'read',
text: strings.Read,
},
{
key: 'write',
text: strings.Write,
},
{
key: 'read-write',
text: strings.ReadWrite,
}
]} />
<DialogFooter>
<PrimaryButton
onClick={() => { props.isDeleteMode ? setMode({ ...mode, delete: !mode.delete }) : setMode({ ...mode, add: !mode.add }) }}
text={props.isDeleteMode ? strings.Remove : strings.Grant} />
<DefaultButton onClick={(() => props.hideDialog(true))} text={strings.Cancel} />
</DialogFooter>
</Dialog>
</>
);
};

View File

@ -0,0 +1,107 @@
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 { IAppListItem, IAzureApp } from './IAppInterfaces';
import { ICommandBarItemProps, CommandBar, MessageBarType } from 'office-ui-fabric-react';
import styles from './SitesSelectedManager.module.scss';
import { AppDialog } from './AppDialog';
import { IObjectWithKey } from '@uifabric/utilities';
import * as strings from 'SitesSelectedManagerWebPartStrings';
import { WebPartContext } from '@microsoft/sp-webpart-base';
interface IAppListProps {
applications: Array<IAzureApp>;
wpContext: WebPartContext;
showMessage: (type: MessageBarType, message: string, autoDismiss: boolean, error?: any) => void;
}
interface IAppListState {
items?: IAppListItem[];
selectionDetails?: IAzureApp;
menuItems?: ICommandBarItemProps[];
dialogHidden?: boolean,
isDeleteMode?: boolean;
commandBarDisabled: boolean;
}
export const AppList: React.FunctionComponent<IAppListProps> = (props) => {
const [state, setState] = React.useState<IAppListState>(
{ dialogHidden: true, isDeleteMode: false, commandBarDisabled: true, selectionDetails: {} as IAzureApp }
);
const [selectedApp, setSelectedApp] = React.useState<IObjectWithKey[]>();
const [selection] = React.useState<Selection>(new Selection({
onSelectionChanged: () => setSelectedApp(selection.getSelection()
)
}))
React.useEffect(() => {
if (selectedApp) {
if (selectedApp.length === 0) {
setState({ ...state, commandBarDisabled: true });
} else {
const app = selectedApp[0] as any;
setState({ ...state, selectionDetails: { id: app.value, displayName: app.name }, commandBarDisabled: false });
}
}
}, [selectedApp])
const hideDialog = (hide: boolean) => {
setState({ ...state, dialogHidden: hide });
};
return <div>
<div>
<CommandBar className={styles.commandBar}
items={[
{
key: 'newItem',
text: strings.ListCommandBarAdd,
iconProps: { iconName: 'CloudAdd' },
split: false,
onClick: () => { setState({ ...state, dialogHidden: false, isDeleteMode: false }) },
disabled: state.commandBarDisabled,
},
{
key: 'upload',
text: strings.ListCommandBarDelete,
iconProps: { iconName: 'BlockedSiteSolid12' },
split: false,
onClick: () => { setState({ ...state, dialogHidden: false, isDeleteMode: true }) },
disabled: state.commandBarDisabled,
}
]}
/>
</div>
<MarqueeSelection className={styles.listMargin} selection={selection}>
<DetailsList
items={props.applications.map(app => ({
key: 0,
name: app.displayName,
value: app.appId
}))}
columns={[
{ key: 'column1', name: strings.ListColAppName, fieldName: 'name', minWidth: 100, maxWidth: 200, isResizable: true },
{ key: 'column2', name: strings.ListColAppId, fieldName: 'value', minWidth: 100, maxWidth: 200, isResizable: true },
]}
setKey="set"
layoutMode={DetailsListLayoutMode.justified}
selection={selection}
checkboxVisibility={CheckboxVisibility.onHover}
selectionMode={SelectionMode.single}
selectionPreservedOnEmptyClick={true}
/>
</MarqueeSelection>
<AppDialog {...
{
isHidden: state.dialogHidden,
hideDialog: hideDialog,
wpContext: props.wpContext,
selectedApp: state.selectionDetails,
isDeleteMode: state.isDeleteMode,
showMessage: props.showMessage
}} />
</div>
}

View File

@ -0,0 +1,37 @@
export interface IPermission {
id?: string;
grantedToIdentities: IPermissionIdentity[];
roles: string[];
}
export interface IPermissionIdentity {
application: IAzureApp
}
export interface ISharePointSite {
displayName: string;
id: string;
}
export interface IAzureApp {
id?: string; //objectId
appId?: string; //clientId
displayName: string;
requiredResourceAccess?: IRequiredResourceAccess[];
}
export interface IRequiredResourceAccess {
resourceAppId: string;
resourceAccess: IResourceAccess[];
}
export interface IResourceAccess {
id: string;
type: string;
}
export interface IAppListItem {
key: number;
name: string;
value: string;
}

View File

@ -1,74 +0,0 @@
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

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

View File

@ -1,168 +0,0 @@
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

@ -1,213 +0,0 @@
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

@ -1,142 +0,0 @@
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

@ -4,6 +4,13 @@
margin-bottom: 50px; margin-bottom: 50px;
} }
.dialogHidden{
display: none;
}
.dialogShow{
display: block;
}
.commandBar { .commandBar {
margin-top: 20px; margin-top: 20px;

View File

@ -1,15 +0,0 @@
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

@ -1,7 +1,70 @@
define([], function() { define([], function () {
return { return {
"DescriptionFieldLabel": "Webpart Title", "DescriptionFieldLabel": "Webpart Title",
"ShowAboutFieldLabel":"Show Home / About Tab", "ShowAboutFieldLabel": "Show Home / About Tab",
"AADGuidLabel":"Sites Selected Permission GUID" "AADGuidLabel": "Sites Selected Permission GUID",
"ErrorNoAppsFoundMessage": `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`,
"ErrorGettingApps": "Unknown error occured getting your apps",
"ErrorHintGettingApps": "- have you consented this web part in API management?",
"ErrorHttp": "Http error occured",
"ErrorGeneric": "Error occured",
"ErrorUnknown": "Error occured",
"ErrorNoPermissionsFound": "No permissions found for removal for app:",
"ErrorHintUrlFormat": `- Check the format of your URL
Correct format below:
https://tenant.sharepoint.com/sites/thesite`,
"HomeTabTitle": "Home / About",
"HomeTitleMain": "What can this webpart do?",
"HomeTitleFYI": "Good to know",
"HomeFYI": `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)`,
"HomeAccessTitle": "User access",
"HomeAccess": "In order to grant access for an app, the user of this webpart has to be a Site Collection Administrator of the site.",
"HomeBulletList": "List Azure AD applications that have the Microsoft graph api scope [Sites.Selected]",
"HomeBulletAdd": "Add SharePoint sites to the listed apps which will enable the app to interact with these sites via the graph api",
"HomeBulletClear": "Clear all SharePoint site permissions for the selected app",
"HomeBulletCheck": "Check what app(s) that has been added to a specific SharePoint site",
"AddTabTitle": "Add/Remove sites to Apps",
"CheckTabTitle": "Check app permissions on a site",
"CheckSiteLabel": "SharePoint site",
"CheckSitePlaceholder": "Please enter URL here",
"CheckButtonText": "Check permission",
"CheckTextAreaLabel": "(Raw) - Permission object for site",
"DialogAddSuccess": "Yay! - Permissions successfully added!",
"DialogRemoveSuccess": "Yay! - Permissions successfully removed!",
"DialogAddTitle": "Grant access to the selected app to a SharePoint site collection",
"DialogAddSubTitle": "Enter a SharePoint site collection URL into the text field and select the wanted access level",
"DialogDelTitle": "Remove the access for the selected app to a SharePoint site collection",
"DialogDelSubTitle": "Enter a SharePoint site collection URL into the text field and click \"remove\" to remove the access",
"ListCommandBarAdd": "Add app permissions",
"ListCommandBarDelete": "Clear app permissions",
"ListColAppName": "App Name",
"ListColAppId": "Azure AD App Id",
"PermCheckTitle": "",
"PermCheckHint": "If the result box shows [] it means there is no permissions granted",
"LoadingMessage": "...loading",
"Close": "Close",
"WorkingOnIt": "Working on it...",
"Read": "Read",
"Write": "Write",
"ReadWrite": "Read / Write",
"Remove": "Remove",
"Grant": "Grant",
"Cancel": "Cancel",
"Info": "Info"
} }
}); });

View File

@ -1,7 +1,62 @@
declare interface ISitesSelectedManagerWebPartStrings { declare interface ISitesSelectedManagerWebPartStrings {
DescriptionFieldLabel: string; DescriptionFieldLabel: string;
ShowAboutFieldLabel: string; ShowAboutFieldLabel: string;
AADGuidLabel:string; AADGuidLabel: string;
ErrorNoAppsFoundMessage: string;
ErrorGettingApps: string;
ErrorHintGettingApps: string;
ErrorHttp: string;
ErrorGeneric: string,
ErrorUnknown: string,
ErrorNoPermissionsFound: string,
ErrorHintUrlFormat: string,
HomeTabTitle: string;
HomeTitleMain: string;
HomeBulletList: string;
HomeBulletAdd: string;
HomeBulletClear: string;
HomeBulletCheck: string;
HomeTitleFYI: string;
HomeFYI: string;
HomeAccessTitle: string;
HomeAccess: string;
AddTabTitle: string;
CheckTabTitle: string;
CheckSiteLabel: string;
CheckSitePlaceholder: string;
CheckButtonText: string;
CheckTextAreaLabel: string;
DialogAddSuccess: string;
DialogRemoveSuccess: string;
DialogAddTitle: string
DialogAddSubTitle: string
DialogDelTitle: string
DialogDelSubTitle: string
ListCommandBarAdd: string;
ListCommandBarDelete: string;
ListColAppName: string;
ListColAppId: string;
PermCheckTitle: string;
PermCheckHint: string;
LoadingMessage: string;
Close: string;
WorkingOnIt: string;
Read: string;
Write: string;
ReadWrite: string;
Remove: string;
Grant: string;
Cancel: string;
Info: string;
} }
declare module 'SitesSelectedManagerWebPartStrings' { declare module 'SitesSelectedManagerWebPartStrings' {