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 | | Version | Date | Comments |
| ------- | ---------------- | --------------- | | ------- | ---------------- | --------------- |
| 1.0 | March 23, 2023 | Initial release | | 1.0 | March 23, 2023 | Initial release |
| 2.0 | July 11, 2023 | Add minor features|
## Minimal Path to Awesome ## 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." "This sample web part shows a report of the web parts used on the current site."
], ],
"creationDateTime": "2023-05-13", "creationDateTime": "2023-05-13",
"updateDateTime": "2023-05-13", "updateDateTime": "2023-07-11",
"products": [ "products": [
"SharePoint" "SharePoint"
], ],

View File

@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "react-graph-webpart-report-client-side-solution", "name": "react-graph-webpart-report-client-side-solution",
"id": "d5339db5-8abe-451a-8afe-57a16de5d286", "id": "d5339db5-8abe-451a-8afe-57a16de5d286",
"version": "1.0.0.0", "version": "2.0.0.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": true, "skipFeatureDeployment": true,
"isDomainIsolated": false, "isDomainIsolated": false,

View File

@ -1,6 +1,6 @@
{ {
"name": "react-graph-webpart-report", "name": "react-graph-webpart-report",
"version": "0.0.1", "version": "2.0.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=16.13.0 <17.0.0" "node": ">=16.13.0 <17.0.0"

View File

@ -1,62 +1,49 @@
import { MSGraphClientV3 } from "@microsoft/sp-http"; 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 { export interface IGraphService {
// eslint-disable-next-line @typescript-eslint/no-explicit-any GetWebParts(siteId: string, pageId: string): Promise<GraphWebPartCollection>;
GetWebParts(client: MSGraphClientV3, siteId: string, pageId: string): Promise<any>; GetSitePages(siteId: string): Promise<GraphSitePage[]>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
GetSitePages(client: MSGraphClientV3, siteId: string): Promise<any>;
} }
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(siteId: string, pageId: string): Promise<GraphWebPartCollection> {
public async GetWebParts(client: MSGraphClientV3, siteId: string, pageId: string): Promise<any> { try {
try{ const client = await this.Get_Client();
// eslint-disable-next-line @typescript-eslint/no-explicit-any const retrievedWebParts: GraphWebPartCollection = await client.api("sites/" + siteId + "/pages/microsoft.graph.sitePage/" + pageId + "/webparts").version('beta').get();
const rawWebParts: any = await this.GET(client, "sites/" + siteId + "/pages/" + pageId + "/webparts","",""); return retrievedWebParts;
return rawWebParts; } catch (error) {
} catch (error){
return null; return null;
} }
} }
public async GetSitePages(client: MSGraphClientV3, siteId: string): Promise<SitePage[]> { public async GetSitePages(siteId: string): Promise<GraphSitePage[]> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const pages: GraphSitePage[] = [];
const rawPages: any = await this.GET(client, "sites/" + siteId + "/pages", "", "id,title"); const client = await this.Get_Client();
// eslint-disable-next-line @typescript-eslint/no-explicit-any const retrievedPages: GraphSitePageCollection = await client.api("sites/" + siteId + "/pages/microsoft.graph.sitePage").select("id,title").version('beta').get();
return rawPages.value.flatMap((rawPage: any) => ( retrievedPages.value.forEach(page => {
[ pages.push(
{ {
id: rawPage.id, id: page.id,
title: rawPage.title 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 { GraphWebPartCollection, WebPart } from "./types";
import { GraphServiceInstance } from "./GraphService"; import { IGraphService } from "./GraphService";
import { MSGraphClientV3 } from "@microsoft/sp-http"; 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 { try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const siteWebParts: WebPart[] = [];
const siteWebParts: any = []; const sitePages: SitePage[] = await service.GetSitePages(siteId);
const sitePages: SitePage[] = await GraphServiceInstance.GetSitePages(graphClient, siteId); for (let i: number = 0; i <= sitePages.length - 1; i++) {
for (let i: number = 0; i<sitePages.length-1; i++){ const graphWebParts: GraphWebPartCollection | null = await service.GetWebParts(siteId, sitePages[i].id);
// eslint-disable-next-line @typescript-eslint/no-explicit-any if (graphWebParts !== null) {
const r: any = await GraphServiceInstance.GetWebParts(graphClient, siteId, sitePages[i].id); graphWebParts.value.forEach(siteWebPart => {
if (r !== null){ siteWebParts.push(
siteWebParts.push(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
r.value.flatMap((siteWebPart: any) => ([
{ {
siteId: siteId, siteId: siteId,
pageTitle: sitePages[i].title, pageTitle: sitePages[i].title,
id: siteWebPart.id, 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;
return siteWebParts.flatMap((t: any)=>t);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return null; return null;

View File

@ -5,12 +5,38 @@ export type WebPart = {
title: string; title: string;
} }
export type AggredatedWebParts = { export type GraphWebPart = {
titles: string[]; data?: GraphWebPartData;
count: number[]; 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; id: string;
title: string; title: string;
} }
@ -22,6 +48,6 @@ export type ChartDataCustom = {
export type DataSet = { export type DataSet = {
label: string; label: string;
data: number []; data: number[];
//backgroundColor: string[]; //backgroundColor: string[];
} }

View File

@ -11,7 +11,7 @@ import * as strings from 'WebPartReportWebPartStrings';
import WebPartReport from './components/WebPartReport'; import WebPartReport from './components/WebPartReport';
import { IWebPartReportProps } from './components/IWebPartReportProps'; import { IWebPartReportProps } from './components/IWebPartReportProps';
import { ITopActions, TopActionsFieldType } from '@microsoft/sp-top-actions'; import { ITopActions, TopActionsFieldType } from '@microsoft/sp-top-actions';
import { MSGraphClientV3 } from "@microsoft/sp-http"; import {GraphService} from "./../GraphService"
export interface IWebPartReportWebPartProps { export interface IWebPartReportWebPartProps {
description: string; description: string;
@ -20,7 +20,6 @@ export interface IWebPartReportWebPartProps {
} }
export default class WebPartReportWebPart extends BaseClientSideWebPart<IWebPartReportWebPartProps> { export default class WebPartReportWebPart extends BaseClientSideWebPart<IWebPartReportWebPartProps> {
private graphClient: MSGraphClientV3;
private _isDarkTheme: boolean = false; private _isDarkTheme: boolean = false;
public render(): void { public render(): void {
@ -33,24 +32,15 @@ export default class WebPartReportWebPart extends BaseClientSideWebPart<IWebPart
hasTeamsContext: !!this.context.sdks.microsoftTeams, hasTeamsContext: !!this.context.sdks.microsoftTeams,
userDisplayName: this.context.pageContext.user.displayName, userDisplayName: this.context.pageContext.user.displayName,
siteId: this.context.pageContext.site.id.toString(), siteId: this.context.pageContext.site.id.toString(),
graphClient: this.graphClient GraphService: new GraphService(this.context),
} }
); );
ReactDom.render(element, this.domElement); ReactDom.render(element, this.domElement);
} }
protected async onInit(): Promise<void> { // eslint-disable-next-line @typescript-eslint/no-empty-function
// eslint-disable-next-line @typescript-eslint/no-explicit-any protected async onInit(): Promise<void> {}
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));
});
}
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void { protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
if (!currentTheme) { if (!currentTheme) {
@ -111,19 +101,24 @@ export default class WebPartReportWebPart extends BaseClientSideWebPart<IWebPart
targetProperty: 'displayOption', targetProperty: 'displayOption',
properties: { properties: {
options: [{ options: [{
key: '1', key: 'list',
text: 'List', text: 'List',
checked: displayOption.toString() === "1" ? true : false checked: displayOption === "list",
iconProps: {
officeFabricIconFontName: "List"
}
}, { }, {
key: '2', key: 'chart',
text: 'Chart', text: 'Chart',
checked: displayOption.toString() === "2" ? true : false checked: displayOption === "chart",
iconProps: {
officeFabricIconFontName: "DonutChart"
}
}] }]
} }
}], }],
onExecute: (actionName, newValue) =>{ onExecute: (actionName, newValue) =>{
this.properties.displayOption = newValue; this.properties.displayOption = newValue;
console.log("test",displayOption.toString() === "1");
this.render(); this.render();
} }
}; };

View File

@ -1,4 +1,4 @@
import { MSGraphClientV3 } from "@microsoft/sp-http"; import { IGraphService } from "../../GraphService";
export interface IWebPartReportProps { export interface IWebPartReportProps {
description: string; description: string;
@ -7,5 +7,6 @@ export interface IWebPartReportProps {
hasTeamsContext: boolean; hasTeamsContext: boolean;
userDisplayName: string; userDisplayName: string;
siteId: string; siteId: string;
graphClient: MSGraphClientV3; GraphService: IGraphService;
} }

View File

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

View File

@ -2,12 +2,12 @@ import * as React from 'react';
import styles from './WebPartReport.module.scss'; import styles from './WebPartReport.module.scss';
import { IWebPartReportProps } from './IWebPartReportProps'; import { IWebPartReportProps } from './IWebPartReportProps';
import { _getSiteWebParts } from '../../WebPartData'; import { _getSiteWebParts } from '../../WebPartData';
import { WebPart } from '../../types';
import { ListView, IViewField } from "@pnp/spfx-controls-react/lib/ListView"; import { ListView, IViewField } from "@pnp/spfx-controls-react/lib/ListView";
import { IWebPartReportWebPartState } from './IWebPartReportWebPartState'; import { IWebPartReportWebPartState } from './IWebPartReportWebPartState';
import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl'; import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl';
import { ChartData } from 'chart.js'; import { ChartData } from 'chart.js';
import { Spinner } from '@fluentui/react'; import { Spinner } from '@fluentui/react';
import { Pagination } from "@pnp/spfx-controls-react/lib/Pagination";
const _viewFields: IViewField[] = [ 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> { export default class WebPartReport extends React.Component<IWebPartReportProps, IWebPartReportWebPartState> {
@ -60,62 +57,66 @@ export default class WebPartReport extends React.Component<IWebPartReportProps,
this.state = { this.state = {
loading: true, loading: true,
webPartList: [], webPartList: [],
aggregatedWebPartList: { titles: [], count: [] } chartWebPartList: { WPTitles: [], WPCount: [] },
page: 1
}; };
} }
public async componentDidMount():Promise<void> { public async componentDidMount(): Promise<void> {
await this._setChartData(); await this._getWebParts();
} }
private loadingData(): Promise<ChartData> { private loadingData(): Promise<ChartData> {
return new Promise<ChartData>((resolve, _reject) => { return new Promise<ChartData>((resolve, _reject) => {
let countWP:number[] = [];
countWP = this.state.aggregatedWebPartList.count
const data: ChartData = const data: ChartData =
{ {
labels: this.state.aggregatedWebPartList.titles.length > 0 ? this.state.aggregatedWebPartList.titles : [], labels: this.state.chartWebPartList.WPTitles.length > 0 ? this.state.chartWebPartList.WPTitles : [],
datasets: [{ label: "WebParts", data: countWP.length > 0 ? countWP : [] }] datasets: [{
label: "WebParts",
data: this.state.chartWebPartList.WPCount.length > 0 ? this.state.chartWebPartList.WPCount : []
}]
}; };
resolve(data); resolve(data);
}); });
} }
public async _setChartData(): Promise<void> { public async _getWebParts(): Promise<void> {
webPartsCounts = []; const webPartsCounts: number[] = [];
webPartsTitles = []; const webPartsTitles: string[] = [];
aggregatedWebPartData.clear(); 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 => { siteWebParts.forEach(e => {
if (!aggregatedWebPartData.has(e.title)) { if (!webPartMap.has(e.title)) {
aggregatedWebPartData.set(e.title, 1); webPartMap.set(e.title, 1);
} else { } 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); webPartsTitles.push(key);
}); });
this.setState({ this.setState({
webPartList: siteWebParts, webPartList: siteWebParts,
aggregatedWebPartList: { chartWebPartList: {
titles: webPartsTitles, WPTitles: webPartsTitles,
count: webPartsCounts WPCount: webPartsCounts
}, },
loading: false loading: false
}); });
} }
private _getPage(selectedPage: number): void {
this.setState({
page: selectedPage
});
}
public render(): React.ReactElement<IWebPartReportProps> { public render(): React.ReactElement<IWebPartReportProps> {
const { const {
@ -126,22 +127,33 @@ export default class WebPartReport extends React.Component<IWebPartReportProps,
return ( return (
<section className={`${styles.webPartReport} ${hasTeamsContext ? styles.teams : ''}`}> <section className={`${styles.webPartReport} ${hasTeamsContext ? styles.teams : ''}`}>
<div className={this.state.loading ? styles.hiddenComponent : ''}> <div className={this.state.loading ? styles.hiddenComponent : ''}>
<div className={displayOption.toString() === "2" ? styles.hiddenComponent : ''}> <div className={displayOption === "chart" ? styles.hiddenComponent : ''}>
<p className={styles.title}>Web parts list:</p> <p className={styles.title}>List of web parts:</p>
<ListView <ListView
viewFields={_viewFields} 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> </div>
<ChartControl <ChartControl
type={ChartType.Doughnut} type={ChartType.Doughnut}
datapromise={this.loadingData()} datapromise={this.loadingData()}
options={options} options={options}
className={displayOption.toString() === "1" ? styles.hiddenComponent : ''} className={displayOption === "list" ? styles.hiddenComponent : ''}
/> />
</div> </div>
<div className={!this.state.loading ? styles.hiddenComponent : ''}> <div className={!this.state.loading ? styles.hiddenComponent : ''}>
<Spinner label="Loading web parts..." /> <Spinner label="Loading web parts..." />
</div> </div>
</section> </section>
); );