Merge pull request #2085 from a1mery/app-secret-expiration-pagination

[react-graph-secret-expiration] - Add pagination
This commit is contained in:
Hugo Bernier 2021-10-31 14:59:05 -04:00 committed by GitHub
commit d16f505759
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 105 additions and 42 deletions

View File

@ -10,7 +10,7 @@ I got the idea from this great article [Use Power Automate to Notify of Upcoming
## Compatibility ## Compatibility
![SPFx 1.12.1](https://img.shields.io/badge/SPFx-1.12.1-green.svg) ![SPFx 1.12.1](https://img.shields.io/badge/SPFx-1.12.1-green.svg)
![Node.js LTS v14 | LTS v12 | LTS v10](https://img.shields.io/badge/Node.js-LTS%20v14%20%7C%20LTS%20v12%20%7C%20LTS%20v10-green.svg) ![Node.js LTS v12 | LTS v10](https://img.shields.io/badge/Node.js-LTS%20v12%20%7C%20LTS%20v10-green.svg)
![SharePoint Online](https://img.shields.io/badge/SharePoint-Online-yellow.svg) ![SharePoint Online](https://img.shields.io/badge/SharePoint-Online-yellow.svg)
![Teams N/A: Untested with Microsoft Teams](https://img.shields.io/badge/Teams-N%2FA-lightgrey.svg "Untested with Microsoft Teams") ![Teams N/A: Untested with Microsoft Teams](https://img.shields.io/badge/Teams-N%2FA-lightgrey.svg "Untested with Microsoft Teams")
![Workbench Hosted: Does not work with local workbench](https://img.shields.io/badge/Workbench-Hosted-yellow.svg "Does not work with local workbench") ![Workbench Hosted: Does not work with local workbench](https://img.shields.io/badge/Workbench-Hosted-yellow.svg "Does not work with local workbench")
@ -32,6 +32,7 @@ react-graph-app-secret-expiration | [Aimery Thomas](https://github.com/a1mery) (
Version|Date|Comments Version|Date|Comments
-------|----|-------- -------|----|--------
1.0|September 17, 2021|Initial release 1.0|September 17, 2021|Initial release
1.1|October 10, 2021|Add pagination
## Minimal Path to Awesome ## Minimal Path to Awesome
@ -39,14 +40,14 @@ Version|Date|Comments
- Clone this repository - Clone this repository
- Ensure that you are at the solution folder - Ensure that you are at the solution folder
- In the command-line run: - In the command-line run:
- **npm install** - `npm install`
- **gulp bundle** - `gulp bundle`
- **gulp package-solution** - `gulp package-solution`
- Deploy the package to your app catalog - Deploy the package to your app catalog
- Approve the API permission request from the SharePoint admin - Approve the API permission request from the SharePoint admin
- Add the web part to a page - Add the web part to a page
- In the command-line run: - In the command-line run:
- **gulp serve --nobrowser** - `gulp serve --nobrowser`
## Features ## Features

View File

@ -9,7 +9,7 @@
"This sample web part shows the list of your applications registered in Azure AD along with their associated client secret/certificate expiration date." "This sample web part shows the list of your applications registered in Azure AD along with their associated client secret/certificate expiration date."
], ],
"creationDateTime": "2021-09-17", "creationDateTime": "2021-09-17",
"updateDateTime": "2021-09-17", "updateDateTime": "2021-10-10",
"products": [ "products": [
"SharePoint", "SharePoint",
"Office" "Office"
@ -22,6 +22,10 @@
{ {
"key": "SPFX-VERSION", "key": "SPFX-VERSION",
"value": "1.12.1" "value": "1.12.1"
},
{
"key": "PNPCONTROLS",
"value": "Pagination"
} }
], ],
"thumbnails": [ "thumbnails": [

View File

@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "react-graph-app-secret-expiration-client-side-solution", "name": "react-graph-app-secret-expiration-client-side-solution",
"id": "b25d85a4-7310-408a-a263-25959b0a5b1b", "id": "b25d85a4-7310-408a-a263-25959b0a5b1b",
"version": "1.0.0.0", "version": "1.1.0.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"isDomainIsolated": false, "isDomainIsolated": false,
"skipFeatureDeployment": true, "skipFeatureDeployment": true,

View File

@ -1,6 +1,6 @@
{ {
"name": "react-graph-app-secret-expiration", "name": "react-graph-app-secret-expiration",
"version": "0.0.1", "version": "1.1.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "react-graph-app-secret-expiration", "name": "react-graph-app-secret-expiration",
"version": "0.0.1", "version": "1.1.0",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"scripts": { "scripts": {

View File

@ -5,7 +5,9 @@ import { ListView, IViewField, SelectionMode, GroupOrder, IGrouping } from "@pnp
import { IApplications, IApplication, IFormattedApplication } from '../../../models/IApplication'; import { IApplications, IApplication, IFormattedApplication } from '../../../models/IApplication';
import { IGraphAppSecretExpirationState } from './GraphAppSecretExpirationState'; import { IGraphAppSecretExpirationState } from './GraphAppSecretExpirationState';
import * as moment from 'moment'; import * as moment from 'moment';
import { DefaultButton, Spinner, mergeStyles } from '@fluentui/react'; import { DefaultButton, Spinner, mergeStyles, SearchBox } from '@fluentui/react';
import { Pagination } from "@pnp/spfx-controls-react/lib/pagination";
import * as strings from 'GraphAppSecretExpirationWebPartStrings';
const stackItemHidden = mergeStyles({ const stackItemHidden = mergeStyles({
display: 'none', display: 'none',
@ -57,9 +59,13 @@ export default class GraphAppSecretExpiration extends React.Component<IGraphAppS
super(props); super(props);
this.state = { this.state = {
applications: [], applications: [],
filteredApplications: [],
filterValue: "",
searchFilter: "",
error: undefined, error: undefined,
loading: true, loading: true,
groupByFields: [] groupByFields: [],
page: 1
}; };
} }
@ -113,8 +119,6 @@ export default class GraphAppSecretExpiration extends React.Component<IGraphAppS
var today = (moment(Date.now())).format('DD-MMM-YYYY'); var today = (moment(Date.now())).format('DD-MMM-YYYY');
try { try {
applications.forEach(app => { applications.forEach(app => {
if (app.passwordCredentials.length > 0) {
app.passwordCredentials.forEach(pswd => { app.passwordCredentials.forEach(pswd => {
let daysBeforeExpiration = moment.duration((moment(pswd.endDateTime)).diff(today, 'days'), 'days').asDays(); let daysBeforeExpiration = moment.duration((moment(pswd.endDateTime)).diff(today, 'days'), 'days').asDays();
let formatedApp: IFormattedApplication = { let formatedApp: IFormattedApplication = {
@ -126,8 +130,6 @@ export default class GraphAppSecretExpiration extends React.Component<IGraphAppS
}; };
displayedApplication.push(formatedApp); displayedApplication.push(formatedApp);
}); });
}
if (app.keyCredentials.length > 0) {
app.keyCredentials.forEach(keyCred => { app.keyCredentials.forEach(keyCred => {
let daysBeforeExpiration = moment.duration((moment(keyCred.endDateTime)).diff(today, 'days'), 'days').asDays(); let daysBeforeExpiration = moment.duration((moment(keyCred.endDateTime)).diff(today, 'days'), 'days').asDays();
let formatedApp: IFormattedApplication = { let formatedApp: IFormattedApplication = {
@ -139,16 +141,58 @@ export default class GraphAppSecretExpiration extends React.Component<IGraphAppS
}; };
displayedApplication.push(formatedApp); displayedApplication.push(formatedApp);
}); });
}
}); });
this.setState({ this.setState({
applications: displayedApplication applications: displayedApplication,
filteredApplications: displayedApplication
}); });
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
} }
private _getPage(selectedPage: number) {
this.setState({
page: selectedPage
});
}
private _filterApplication = (value, clear: boolean) => {
let searchResult: IFormattedApplication[] = [];
if (clear) {
this.state.applications.forEach(app => {
if (this._filterByProperties(app, value)) {
searchResult.push(app);
}
});
this.setState({
filteredApplications: searchResult,
filterValue: value
});
} else {
this.setState({
filteredApplications: this.state.applications,
filterValue: value,
page: 1
});
}
}
private _filterByProperties(application: IFormattedApplication, filterValue) {
if (application.applicationId.toLowerCase().indexOf(filterValue.toLowerCase()) >= 0) {
return true;
} else if (application.displayName.toLowerCase().indexOf(filterValue.toLowerCase()) >= 0) {
return true;
} else if (application.expirationDate.toLowerCase().indexOf(filterValue.toLowerCase()) >= 0) {
return true;
} else if (application.type.toLowerCase().indexOf(filterValue.toLowerCase()) >= 0) {
return true;
} else {
return false;
}
}
private _groupView = () => { private _groupView = () => {
if (this.state.groupByFields.length === 0) { if (this.state.groupByFields.length === 0) {
let groupByFields: IGrouping[] = [ let groupByFields: IGrouping[] = [
@ -177,21 +221,31 @@ export default class GraphAppSecretExpiration extends React.Component<IGraphAppS
<p className={styles.title}>Application list :</p> <p className={styles.title}>Application list :</p>
<div className={this.state.loading ? "" : stackItemHidden}> <div className={this.state.loading ? "" : stackItemHidden}>
<Spinner label="Loading..." ariaLive="assertive" labelPosition="left" /> <Spinner label="Loading..." ariaLive="assertive" labelPosition="left" />
<br/> <br />
</div> </div>
<div className={this.state.loading ? stackItemHidden : ""}> <div className={this.state.loading ? stackItemHidden : ""}>
<DefaultButton text={this.state.groupByFields.length === 0 ? "Group by App ID" : "Ungroup"} onClick={this._groupView} /> <DefaultButton text={this.state.groupByFields.length === 0 ? "Group by App ID" : "Ungroup"} onClick={this._groupView} />
<SearchBox placeholder="Search" onChange={(e, text) => this._filterApplication(text, false)} onClear={() => this._filterApplication("", true)} value={this.state.filterValue} />
<ListView <ListView
items={this.state.applications} items={this.state.filteredApplications.slice(this.state.page === 1 || this.state.filterValue !== "" ? 0 : this.state.page * 10 - 10, this.state.page * 10)}
viewFields={_viewFields} viewFields={_viewFields}
iconFieldName="ServerRelativeUrl" iconFieldName="ServerRelativeUrl"
compact={true} compact={true}
selectionMode={SelectionMode.none} selectionMode={SelectionMode.none}
selection={this._getSelection} selection={this._getSelection}
groupByFields={this.state.groupByFields} groupByFields={this.state.groupByFields}
showFilter={true} showFilter={false}
filterPlaceHolder="Search..." /> filterPlaceHolder="Search..." />
</div> </div>
<Pagination
currentPage={1}
totalPages={Math.floor(this.state.filteredApplications.length / 10) + 1}
onChange={(page) => this._getPage(page)}
limiter={3} // Optional - default value 3
hideFirstPageJump // Optional
hideLastPageJump // Optional
limiterIcon={"Emoji12"} // Optional
/>
</div> </div>
</div> </div>
); );

View File

@ -3,7 +3,11 @@ import { IApplications, IApplication,IPasswordCredential,IKeyCredential,IFormatt
export interface IGraphAppSecretExpirationState { export interface IGraphAppSecretExpirationState {
applications: IFormattedApplication[]; applications: IFormattedApplication[];
filteredApplications: IFormattedApplication[];
filterValue: string;
searchFilter: string;
groupByFields: IGrouping[]; groupByFields: IGrouping[];
page: number;
error: string; error: string;
loading: boolean; loading: boolean;
} }