Merge pull request #3795 from a1mery/react-graph-webpart-report-improvements
This commit is contained in:
commit
ac0ed1c33e
|
@ -50,8 +50,7 @@ Microsoft Graph permission:
|
|||
| Version | Date | Comments |
|
||||
| ------- | ---------------- | --------------- |
|
||||
| 1.0 | March 23, 2023 | Initial release |
|
||||
|
||||
|
||||
| 2.0 | July 11, 2023 | Add minor features|
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"This sample web part shows a report of the web parts used on the current site."
|
||||
],
|
||||
"creationDateTime": "2023-05-13",
|
||||
"updateDateTime": "2023-05-13",
|
||||
"updateDateTime": "2023-07-11",
|
||||
"products": [
|
||||
"SharePoint"
|
||||
],
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"solution": {
|
||||
"name": "react-graph-webpart-report-client-side-solution",
|
||||
"id": "d5339db5-8abe-451a-8afe-57a16de5d286",
|
||||
"version": "1.0.0.0",
|
||||
"version": "2.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true,
|
||||
"isDomainIsolated": false,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "react-graph-webpart-report",
|
||||
"version": "0.0.1",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=16.13.0 <17.0.0"
|
||||
|
|
|
@ -1,62 +1,49 @@
|
|||
import { MSGraphClientV3 } from "@microsoft/sp-http";
|
||||
import { SitePage } from "./types";
|
||||
|
||||
import { GraphSitePage, GraphSitePageCollection, GraphWebPartCollection } from "./types";
|
||||
import { BaseComponentContext } from "@microsoft/sp-component-base";
|
||||
|
||||
export interface IGraphService {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
GetWebParts(client: MSGraphClientV3, siteId: string, pageId: string): Promise<any>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
GetSitePages(client: MSGraphClientV3, siteId: string): Promise<any>;
|
||||
GetWebParts(siteId: string, pageId: string): Promise<GraphWebPartCollection>;
|
||||
GetSitePages(siteId: string): Promise<GraphSitePage[]>;
|
||||
}
|
||||
|
||||
export class GraphService implements IGraphService {
|
||||
private MSGraphClient: MSGraphClientV3;
|
||||
private Context: BaseComponentContext;
|
||||
|
||||
constructor(Context: BaseComponentContext) {
|
||||
this.Context = Context;
|
||||
}
|
||||
|
||||
class GraphService implements IGraphService {
|
||||
private async Get_Client(): Promise<MSGraphClientV3> {
|
||||
if (this.MSGraphClient === undefined)
|
||||
this.MSGraphClient = await this.Context.msGraphClientFactory.getClient("3");
|
||||
return this.MSGraphClient;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public async GetWebParts(client: MSGraphClientV3, siteId: string, pageId: string): Promise<any> {
|
||||
try{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const rawWebParts: any = await this.GET(client, "sites/" + siteId + "/pages/" + pageId + "/webparts","","");
|
||||
return rawWebParts;
|
||||
} catch (error){
|
||||
public async GetWebParts(siteId: string, pageId: string): Promise<GraphWebPartCollection> {
|
||||
try {
|
||||
const client = await this.Get_Client();
|
||||
const retrievedWebParts: GraphWebPartCollection = await client.api("sites/" + siteId + "/pages/microsoft.graph.sitePage/" + pageId + "/webparts").version('beta').get();
|
||||
return retrievedWebParts;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public async GetSitePages(client: MSGraphClientV3, siteId: string): Promise<SitePage[]> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const rawPages: any = await this.GET(client, "sites/" + siteId + "/pages", "", "id,title");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return rawPages.value.flatMap((rawPage: any) => (
|
||||
[
|
||||
public async GetSitePages(siteId: string): Promise<GraphSitePage[]> {
|
||||
const pages: GraphSitePage[] = [];
|
||||
const client = await this.Get_Client();
|
||||
const retrievedPages: GraphSitePageCollection = await client.api("sites/" + siteId + "/pages/microsoft.graph.sitePage").select("id,title").version('beta').get();
|
||||
retrievedPages.value.forEach(page => {
|
||||
pages.push(
|
||||
{
|
||||
id: rawPage.id,
|
||||
title: rawPage.title
|
||||
id: page.id,
|
||||
title: page.title
|
||||
}
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private GET(client: MSGraphClientV3, api: string, filter?: string, select?: string, top?: number, responseType?: any): Promise<any> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
client.api(api).version("beta").select(select).filter(filter).responseType(responseType)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.get((error: any, response: any) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
)
|
||||
});
|
||||
return pages;
|
||||
}
|
||||
}
|
||||
export const GraphServiceInstance = new GraphService();
|
||||
|
||||
|
|
|
@ -1,35 +1,28 @@
|
|||
import { SitePage, WebPart } from "./types";
|
||||
import { GraphServiceInstance } from "./GraphService";
|
||||
import { MSGraphClientV3 } from "@microsoft/sp-http";
|
||||
import { GraphWebPartCollection, WebPart } from "./types";
|
||||
import { IGraphService } from "./GraphService";
|
||||
import {SitePage} from "@microsoft/microsoft-graph-types-beta"
|
||||
|
||||
|
||||
export async function _getSiteWebParts(graphClient: MSGraphClientV3, siteId: string): Promise<WebPart[]> {
|
||||
export async function _getSiteWebParts(service: IGraphService, siteId: string): Promise<WebPart[]> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const siteWebParts: any = [];
|
||||
const sitePages: SitePage[] = await GraphServiceInstance.GetSitePages(graphClient, siteId);
|
||||
for (let i: number = 0; i<sitePages.length-1; i++){
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const r: any = await GraphServiceInstance.GetWebParts(graphClient, siteId, sitePages[i].id);
|
||||
if (r !== null){
|
||||
siteWebParts.push(
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
r.value.flatMap((siteWebPart: any) => ([
|
||||
const siteWebParts: WebPart[] = [];
|
||||
const sitePages: SitePage[] = await service.GetSitePages(siteId);
|
||||
for (let i: number = 0; i <= sitePages.length - 1; i++) {
|
||||
const graphWebParts: GraphWebPartCollection | null = await service.GetWebParts(siteId, sitePages[i].id);
|
||||
if (graphWebParts !== null) {
|
||||
graphWebParts.value.forEach(siteWebPart => {
|
||||
siteWebParts.push(
|
||||
{
|
||||
siteId: siteId,
|
||||
pageTitle: sitePages[i].title,
|
||||
id: siteWebPart.id,
|
||||
title: siteWebPart.data.title,
|
||||
title: siteWebPart.innerHtml !== undefined ? "Text" : siteWebPart.data.title,
|
||||
}
|
||||
]))
|
||||
);
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return siteWebParts.flatMap((t: any)=>t);
|
||||
|
||||
|
||||
return siteWebParts;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
|
|
|
@ -5,12 +5,38 @@ export type WebPart = {
|
|||
title: string;
|
||||
}
|
||||
|
||||
export type AggredatedWebParts = {
|
||||
titles: string[];
|
||||
count: number[];
|
||||
export type GraphWebPart = {
|
||||
data?: GraphWebPartData;
|
||||
id: string;
|
||||
webPartType: string;
|
||||
innerHtml?: string;
|
||||
}
|
||||
|
||||
export type SitePage = {
|
||||
export type GraphWebPartCollection = {
|
||||
value: GraphWebPart[];
|
||||
}
|
||||
|
||||
export type GraphWebPartData = {
|
||||
audiences: string[];
|
||||
dataVersion: string[];
|
||||
description: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
properties: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
serverProcessedContent: any;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export type AggredatedWebParts = {
|
||||
WPTitles: string[];
|
||||
WPCount: number[];
|
||||
}
|
||||
|
||||
export type GraphSitePageCollection = {
|
||||
value: GraphSitePage[];
|
||||
}
|
||||
|
||||
export type GraphSitePage = {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
@ -22,6 +48,6 @@ export type ChartDataCustom = {
|
|||
|
||||
export type DataSet = {
|
||||
label: string;
|
||||
data: number [];
|
||||
data: number[];
|
||||
//backgroundColor: string[];
|
||||
}
|
|
@ -11,7 +11,7 @@ import * as strings from 'WebPartReportWebPartStrings';
|
|||
import WebPartReport from './components/WebPartReport';
|
||||
import { IWebPartReportProps } from './components/IWebPartReportProps';
|
||||
import { ITopActions, TopActionsFieldType } from '@microsoft/sp-top-actions';
|
||||
import { MSGraphClientV3 } from "@microsoft/sp-http";
|
||||
import {GraphService} from "./../GraphService"
|
||||
|
||||
export interface IWebPartReportWebPartProps {
|
||||
description: string;
|
||||
|
@ -20,7 +20,6 @@ export interface IWebPartReportWebPartProps {
|
|||
}
|
||||
|
||||
export default class WebPartReportWebPart extends BaseClientSideWebPart<IWebPartReportWebPartProps> {
|
||||
private graphClient: MSGraphClientV3;
|
||||
private _isDarkTheme: boolean = false;
|
||||
|
||||
public render(): void {
|
||||
|
@ -33,24 +32,15 @@ export default class WebPartReportWebPart extends BaseClientSideWebPart<IWebPart
|
|||
hasTeamsContext: !!this.context.sdks.microsoftTeams,
|
||||
userDisplayName: this.context.pageContext.user.displayName,
|
||||
siteId: this.context.pageContext.site.id.toString(),
|
||||
graphClient: this.graphClient
|
||||
GraphService: new GraphService(this.context),
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected async onInit(): Promise<void> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return new Promise<void>((resolve: () => void, reject: (error: any) => void): void => {
|
||||
this.context.msGraphClientFactory
|
||||
.getClient("3")
|
||||
.then((client: MSGraphClientV3): void => {
|
||||
this.graphClient = client;
|
||||
resolve();
|
||||
}, err => reject(err));
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
protected async onInit(): Promise<void> {}
|
||||
|
||||
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
|
||||
if (!currentTheme) {
|
||||
|
@ -111,19 +101,24 @@ export default class WebPartReportWebPart extends BaseClientSideWebPart<IWebPart
|
|||
targetProperty: 'displayOption',
|
||||
properties: {
|
||||
options: [{
|
||||
key: '1',
|
||||
key: 'list',
|
||||
text: 'List',
|
||||
checked: displayOption.toString() === "1" ? true : false
|
||||
checked: displayOption === "list",
|
||||
iconProps: {
|
||||
officeFabricIconFontName: "List"
|
||||
}
|
||||
}, {
|
||||
key: '2',
|
||||
key: 'chart',
|
||||
text: 'Chart',
|
||||
checked: displayOption.toString() === "2" ? true : false
|
||||
checked: displayOption === "chart",
|
||||
iconProps: {
|
||||
officeFabricIconFontName: "DonutChart"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}],
|
||||
onExecute: (actionName, newValue) =>{
|
||||
this.properties.displayOption = newValue;
|
||||
console.log("test",displayOption.toString() === "1");
|
||||
this.render();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { MSGraphClientV3 } from "@microsoft/sp-http";
|
||||
import { IGraphService } from "../../GraphService";
|
||||
|
||||
export interface IWebPartReportProps {
|
||||
description: string;
|
||||
|
@ -7,5 +7,6 @@ export interface IWebPartReportProps {
|
|||
hasTeamsContext: boolean;
|
||||
userDisplayName: string;
|
||||
siteId: string;
|
||||
graphClient: MSGraphClientV3;
|
||||
GraphService: IGraphService;
|
||||
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { AggredatedWebParts, WebPart } from "../../types";
|
|||
|
||||
export interface IWebPartReportWebPartState {
|
||||
webPartList: WebPart[];
|
||||
aggregatedWebPartList: AggredatedWebParts;
|
||||
chartWebPartList: AggredatedWebParts;
|
||||
loading: boolean;
|
||||
page: number;
|
||||
}
|
|
@ -2,12 +2,12 @@ import * as React from 'react';
|
|||
import styles from './WebPartReport.module.scss';
|
||||
import { IWebPartReportProps } from './IWebPartReportProps';
|
||||
import { _getSiteWebParts } from '../../WebPartData';
|
||||
import { WebPart } from '../../types';
|
||||
import { ListView, IViewField } from "@pnp/spfx-controls-react/lib/ListView";
|
||||
import { IWebPartReportWebPartState } from './IWebPartReportWebPartState';
|
||||
import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl';
|
||||
import { ChartData } from 'chart.js';
|
||||
import { Spinner } from '@fluentui/react';
|
||||
import { Pagination } from "@pnp/spfx-controls-react/lib/Pagination";
|
||||
|
||||
const _viewFields: IViewField[] = [
|
||||
{
|
||||
|
@ -48,10 +48,7 @@ const options: any = {
|
|||
};
|
||||
|
||||
|
||||
let siteWebParts: WebPart[];
|
||||
let webPartsCounts: number[] = [];
|
||||
let webPartsTitles: string[] = [];
|
||||
const aggregatedWebPartData = new Map<string, number>();
|
||||
|
||||
|
||||
export default class WebPartReport extends React.Component<IWebPartReportProps, IWebPartReportWebPartState> {
|
||||
|
||||
|
@ -60,62 +57,66 @@ export default class WebPartReport extends React.Component<IWebPartReportProps,
|
|||
this.state = {
|
||||
loading: true,
|
||||
webPartList: [],
|
||||
aggregatedWebPartList: { titles: [], count: [] }
|
||||
chartWebPartList: { WPTitles: [], WPCount: [] },
|
||||
page: 1
|
||||
};
|
||||
}
|
||||
|
||||
public async componentDidMount():Promise<void> {
|
||||
await this._setChartData();
|
||||
public async componentDidMount(): Promise<void> {
|
||||
await this._getWebParts();
|
||||
}
|
||||
|
||||
private loadingData(): Promise<ChartData> {
|
||||
|
||||
return new Promise<ChartData>((resolve, _reject) => {
|
||||
|
||||
let countWP:number[] = [];
|
||||
countWP = this.state.aggregatedWebPartList.count
|
||||
const data: ChartData =
|
||||
{
|
||||
labels: this.state.aggregatedWebPartList.titles.length > 0 ? this.state.aggregatedWebPartList.titles : [],
|
||||
datasets: [{ label: "WebParts", data: countWP.length > 0 ? countWP : [] }]
|
||||
labels: this.state.chartWebPartList.WPTitles.length > 0 ? this.state.chartWebPartList.WPTitles : [],
|
||||
datasets: [{
|
||||
label: "WebParts",
|
||||
data: this.state.chartWebPartList.WPCount.length > 0 ? this.state.chartWebPartList.WPCount : []
|
||||
}]
|
||||
};
|
||||
resolve(data);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public async _setChartData(): Promise<void> {
|
||||
public async _getWebParts(): Promise<void> {
|
||||
|
||||
webPartsCounts = [];
|
||||
webPartsTitles = [];
|
||||
aggregatedWebPartData.clear();
|
||||
const webPartsCounts: number[] = [];
|
||||
const webPartsTitles: string[] = [];
|
||||
const webPartMap = new Map<string, number>();
|
||||
|
||||
siteWebParts = await _getSiteWebParts(this.props.graphClient, this.props.siteId.toString());
|
||||
webPartMap.clear();
|
||||
|
||||
const siteWebParts = await _getSiteWebParts(this.props.GraphService, this.props.siteId.toString());
|
||||
siteWebParts.forEach(e => {
|
||||
if (!aggregatedWebPartData.has(e.title)) {
|
||||
aggregatedWebPartData.set(e.title, 1);
|
||||
if (!webPartMap.has(e.title)) {
|
||||
webPartMap.set(e.title, 1);
|
||||
} else {
|
||||
aggregatedWebPartData.set(e.title, aggregatedWebPartData.get(e.title) + 1)
|
||||
webPartMap.set(e.title, webPartMap.get(e.title) + 1)
|
||||
}
|
||||
});
|
||||
aggregatedWebPartData.forEach(a => {
|
||||
webPartsCounts.push(a);
|
||||
});
|
||||
|
||||
aggregatedWebPartData.forEach((value, key) => {
|
||||
webPartMap.forEach((value, key) => {
|
||||
webPartsCounts.push(value);
|
||||
webPartsTitles.push(key);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
webPartList: siteWebParts,
|
||||
aggregatedWebPartList: {
|
||||
titles: webPartsTitles,
|
||||
count: webPartsCounts
|
||||
chartWebPartList: {
|
||||
WPTitles: webPartsTitles,
|
||||
WPCount: webPartsCounts
|
||||
},
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
|
||||
private _getPage(selectedPage: number): void {
|
||||
this.setState({
|
||||
page: selectedPage
|
||||
});
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IWebPartReportProps> {
|
||||
const {
|
||||
|
@ -126,22 +127,33 @@ export default class WebPartReport extends React.Component<IWebPartReportProps,
|
|||
return (
|
||||
<section className={`${styles.webPartReport} ${hasTeamsContext ? styles.teams : ''}`}>
|
||||
<div className={this.state.loading ? styles.hiddenComponent : ''}>
|
||||
<div className={displayOption.toString() === "2" ? styles.hiddenComponent : ''}>
|
||||
<p className={styles.title}>Web parts list:</p>
|
||||
<div className={displayOption === "chart" ? styles.hiddenComponent : ''}>
|
||||
<p className={styles.title}>List of web parts:</p>
|
||||
<ListView
|
||||
viewFields={_viewFields}
|
||||
items={siteWebParts}
|
||||
items={this.state.webPartList.slice(this.state.page === 1 ? 0 : this.state.page * 10 - 10, this.state.page * 10)}
|
||||
showFilter={true}
|
||||
filterPlaceHolder="Search..."
|
||||
/>
|
||||
<Pagination
|
||||
currentPage={1}
|
||||
totalPages={Math.floor(this.state.webPartList.length / 10) + 1}
|
||||
onChange={(page) => this._getPage(page)}
|
||||
limiter={3} // Optional - default value 3
|
||||
hideFirstPageJump // Optional
|
||||
hideLastPageJump // Optional
|
||||
limiterIcon={"Emoji12"} // Optional
|
||||
/>
|
||||
</div>
|
||||
<ChartControl
|
||||
type={ChartType.Doughnut}
|
||||
datapromise={this.loadingData()}
|
||||
options={options}
|
||||
className={displayOption.toString() === "1" ? styles.hiddenComponent : ''}
|
||||
className={displayOption === "list" ? styles.hiddenComponent : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className={!this.state.loading ? styles.hiddenComponent : ''}>
|
||||
<Spinner label="Loading web parts..." />
|
||||
<Spinner label="Loading web parts..." />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue