Merge pull request #3795 from a1mery/react-graph-webpart-report-improvements

This commit is contained in:
Hugo Bernier 2023-08-09 23:11:51 -04:00 committed by GitHub
commit ac0ed1c33e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 147 additions and 133 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { AggredatedWebParts, WebPart } from "../../types";
export interface IWebPartReportWebPartState {
webPartList: WebPart[];
aggregatedWebPartList: AggredatedWebParts;
chartWebPartList: AggredatedWebParts;
loading: boolean;
page: number;
}

View File

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