Merge pull request #1280 from sudharsank/react-appinsights-dashboard

This commit is contained in:
Hugo Bernier 2020-05-16 00:27:28 -04:00 committed by GitHub
commit 8a189caa10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 18893 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.10.0",
"libraryName": "react-appinsights-dashboard",
"libraryId": "6696970c-955e-4d95-82a3-35c0d2a5818c",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,64 @@
# React AppInsights Dashboard
## Summary
> This webpart displays different statistics data captured in the **Azure Application Insights** in a graphical representation. Filters are provided to search for certain period of days. There are few **Application Customizer** which can be activated in **SharePoint Online** to track page view, performance etc., to **Azure Application Insights**, but the data can be viewed only by the administrator who is in-charge of **Azure portal**. Not all the users will have access to this data, this webpart will provide access to those data that can be used by the portal administrators and developers to keep track of the page performance and hits. Fetched insights data using **[Application Insights API](https://dev.applicationinsights.io/)**.
## Pre-requisites
> **Azure Application Insights** has to be configured. If you want to track the **SharePoint Online** webparts and pages, please use either of the following **Application Customizer** or you can use your own extensions to track the pages and other components.
* [Injecting Javascript with Sharepoint Framework Extensions - Azure Application Insights](https://github.com/pnp/sp-dev-fx-extensions/tree/master/samples/js-application-appinsights)
* [JS Application AppInsights Advanced](https://github.com/pnp/sp-dev-fx-extensions/tree/master/samples/js-application-appinsights-advanced)
> Following are required to access the data using **[App Insights API](https://dev.applicationinsights.io/)**. The API has been provided in a very simple way with **[API Explorer](https://dev.applicationinsights.io/apiexplorer)** for the developers to play around the API to understand the schema and the methods that can used.
* **Application ID** of the Application Insights
* **API Key** for the data access
![AppInsights API Application ID](./assets/AppInsights_APIAccess.png)
![AppInsights API Key](./assets/AppInsights_APIKey.png)
## Properties
* **_Application ID_**: Application ID of the Azure Application Insights API Access.
* **_Application Key_**: Application Key of the Azure Application Insights API Access.
## Preview
#### AppInsights Dashboard
![AppInsights Dashboard](./assets/AppInsights_Dashboard.gif)
#### Page Statistics
![AppInsights Dashboard - Page Statistics](./assets/PageStatistics.png)
#### User Statistics
![AppInsights Dashboard - User Statistics](./assets/UserStatistics.png)
#### Performance Statistics
![AppInsights Dashboard - Performance Statistics](./assets/PerformanceStatistics.png)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## SharePoint Frameword Pre-requisites
> **@microsoft/generator-sharepoint - 1.10.0**
## Solution
Solution|Author(s)
--------|---------
react-appinsights-dashboard | Sudharsan K.([@sudharsank](https://twitter.com/sudharsank), [Know More](http://windowssharepointserver.blogspot.com/))
## Version history
Version|Date|Comments
-------|----|--------
1.0.0.0|May 10 2020|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
## Minimal Path to Awesome
- Clone this repository
- in the command line run:
- `npm install`
- `gulp bundle --ship && gulp package-solution --ship`
#### Local Mode
This solution doesn't work on local mode.

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"app-insights-dashboard-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/appInsightsDashboard/AppInsightsDashboardWebPart.js",
"manifest": "./src/webparts/appInsightsDashboard/AppInsightsDashboardWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"AppInsightsDashboardWebPartStrings": "lib/webparts/appInsightsDashboard/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-appinsights-dashboard",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "React AppInsights Dashboard",
"id": "6696970c-955e-4d95-82a3-35c0d2a5818c",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/react-appinsights-dashboard.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,7 @@
'use strict';
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
{
"name": "react-appinsights-dashboard",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@pnp/spfx-controls-react": "1.17.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"lodash": "^4.17.15",
"moment": "^2.25.3",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1,105 @@
.dataLabel {
padding-right: 3px;
font-size: 14px;
font-weight: 600;
}
.pivotControl {
margin-top: -4px;
div[role="tablist"] {
border: 1px solid #7f7f7f;
border-radius: 12px;
padding: 1px;
}
button {
padding: 2px 8px 3px;
height: 25px;
cursor: pointer;
text-align: center;
border-radius: 10px;
background-clip: padding-box;
overflow: hidden;
text-overflow: ellipsis;
border: 1px solid transparent!important;
font-size: 13px;
margin-right: 0px;
&:hover {
background-color: lightgrey;
border-color: lightgrey;
}
&[aria-selected="true"] {
background-color: #0078d4;
border-color: #0078d4;
color: #fff;
fill: #fff;
}
}
}
.centerDiv {
display: flex;
width: auto;
margin: 0 auto;
}
.secTitleContainer {
display: flex;
padding-bottom: 10px;
.title {
font-size: 18px;
font-weight: bold;
display: flex;
width: auto;
margin: 0 auto;
}
}
.content {
margin-top: 10px !important;
}
.fileiconDiv {
width: 3%;
display: inline-block;
padding-right: 5px;
i {
position: absolute;
top: 8px;
}
}
.pageLink {
text-decoration: none;
font-size: 12px;
text-overflow: ellipsis;
overflow: hidden;
&:hover {
font-weight: 600;
text-decoration: underline;
}
}
.chartContainer {
height: 350px;
width: 100%;
}
.chart {
height: 358px !important;
max-height: 358px !important;
canvas {
height: 358px !important;
max-height: 358px !important;
}
}
.chartPerf {
height: 400px !important;
max-height: 400px !important;
canvas {
height: 400px !important;
max-height: 400px !important;
}
}
.textWithIcon {
display: flex;
overflow: hidden;
text-overflow: ellipsis;
}
.dataList {
overflow-y: auto;
height: 350px;
width: 100%;
}

View File

@ -0,0 +1,24 @@
export const defaultDateFormat: string = "MM/DD/YYYY";
export const chartDateFormat: string = "MMM DD, hh:mm A";
export interface IPageViewCountProps {
oriDate: string;
date: string;
sum: number;
}
export interface IPageViewDetailProps {
oriStartDate: string;
oriEndDate: string;
start: string;
end: string;
date: string;
Url: string;
count: string;
}
export interface IPerfDurationProps {
PageName: string;
count: number;
AvgDuration: number;
PerDur_50: number;
PerDur_95: number;
PerDur_99: number;
}

View File

@ -0,0 +1,164 @@
import { HttpClient, IHttpClientOptions, HttpClientResponse } from '@microsoft/sp-http';
import { TimeInterval, TimeSpan, Segments } from './enumHelper';
import { IPageViewCountProps, IPageViewDetailProps, defaultDateFormat, chartDateFormat } from './CommonProps';
const moment: any = require('moment');
export default class Helper {
private _appid: string = '';
private _appkey: string = '';
private _postUrl: string = `https://api.applicationinsights.io/v1/apps`;
private requestHeaders: Headers = new Headers();
private httpClientOptions: IHttpClientOptions = {};
private httpClient: HttpClient = null;
constructor(appid: string, appkey: string, httpclient: HttpClient) {
this._appid = appid;
this._appkey = appkey;
this.httpClient = httpclient;
this._postUrl = this._postUrl + `/${this._appid}`;
this.requestHeaders.append('Content-type', 'application/json; charset=utf-8');
this.requestHeaders.append('x-api-key', this._appkey);
this.httpClientOptions = { headers: this.requestHeaders };
}
public getPageViewCount = async (timespan: TimeSpan, timeinterval: TimeInterval): Promise<IPageViewCountProps[]> => {
let finalRes: IPageViewCountProps[] = [];
let finalPostUrl: string = this._postUrl + `/metrics/pageViews/count?timespan=${timespan}&interval=${timeinterval}`;
let response: HttpClientResponse = await this.httpClient.get(finalPostUrl, HttpClient.configurations.v1, this.httpClientOptions);
let responseJson: any = await response.json();
if (responseJson.value && responseJson.value.segments.length > 0) {
let segments: any[] = responseJson.value.segments;
segments.map((seg: any) => {
finalRes.push({
oriDate: seg.start,
date: this.getLocalTime(seg.start),
sum: seg['pageViews/count'].sum
});
});
}
return finalRes;
}
public getPageViews = async (timespan: TimeSpan, timeinterval: TimeInterval, segment: Segments[]): Promise<IPageViewDetailProps[]> => {
let finalRes: IPageViewDetailProps[] = [];
let finalPostUrl: string = this._postUrl + `/metrics/pageViews/count?timespan=${timespan}&interval=${timeinterval}&segment=${encodeURIComponent(segment.join(','))}`;
let response: HttpClientResponse = await this.httpClient.get(finalPostUrl, HttpClient.configurations.v1, this.httpClientOptions);
let responseJson: any = await response.json();
if (responseJson.value && responseJson.value.segments.length > 0) {
let mainSegments: any[] = responseJson.value.segments;
mainSegments.map(mainseg => {
if (mainseg.segments.length > 0) {
mainseg.segments.map((seg: any) => {
finalRes.push({
oriStartDate: mainseg.start,
oriEndDate: mainseg.end,
start: this.getFormattedDate(mainseg.start),
end: this.getFormattedDate(mainseg.end),
date: `${this.getFormattedDate(mainseg.start)} - ${this.getFormattedDate(mainseg.end)}`,
Url: seg[segment[0]],
count: seg['pageViews/count'].sum
});
});
}
});
}
return finalRes;
}
public getResponseByQuery = async (query: string, useTimespan: boolean, timespan?: TimeSpan): Promise<any[]> => {
let finalRes: any[] = [];
let urlQuery: string = useTimespan ? `timespan=${timespan}&query=${encodeURIComponent(query)}` : `query=${encodeURIComponent(query)}`;
let finalPostUrl: string = this._postUrl + `/query?${urlQuery}`;
let responseJson: any = await this.getAPIResponse(finalPostUrl);
if (responseJson.tables.length > 0) {
finalRes = responseJson.tables[0].rows;
}
return finalRes;
}
public getUserPageViews = async (timespan: TimeSpan | string, timeinterval: TimeInterval, segment: Segments[]): Promise<IPageViewDetailProps[]> => {
let finalRes: any[] = [];
let finalPostUrl: string = this._postUrl + `/metrics/pageViews/count?timespan=${encodeURIComponent(timespan)}&interval=${timeinterval}&segment=${encodeURIComponent(segment.join(','))}`;
let response: HttpClientResponse = await this.httpClient.get(finalPostUrl, HttpClient.configurations.v1, this.httpClientOptions);
let responseJson: any = await response.json();
if (responseJson.value && responseJson.value.segments.length > 0) {
let mainSegments: any[] = responseJson.value.segments;
mainSegments.map(mainseg => {
if (mainseg.segments.length > 0) {
let childSegments: any[] = mainseg.segments;
childSegments.map(childseg => {
let grandChildSegments: any[] = childseg.segments;
grandChildSegments.map(grandchildseg => {
if (grandchildseg['pageView/urlPath'] != '') {
finalRes.push({
oriStartDate: mainseg.start,
oriEndDate: mainseg.end,
start: this.getFormattedDate(mainseg.start),
end: this.getFormattedDate(mainseg.end),
date: `${this.getFormattedDate(mainseg.start)} - ${this.getFormattedDate(mainseg.end)}`,
Url: grandchildseg['pageView/urlPath'],
count: grandchildseg['pageViews/count'].sum,
user: childseg['customDimensions/UserTitle']
});
}
});
});
}
});
}
return finalRes;
}
public getAPIResponse = async (urlWithQuery: string): Promise<any> => {
let response: HttpClientResponse = await this.httpClient.get(urlWithQuery, HttpClient.configurations.v1, this.httpClientOptions);
return await response.json();
}
public getTimeSpanMenu = (): any[] => {
let items: any[] = [];
Object.keys(TimeSpan).map(key => {
items.push({
text: key,
key: key
});
});
return items;
}
public getTimeIntervalMenu = (): any[] => {
let items: any[] = [];
Object.keys(TimeInterval).map(key => {
items.push({
text: key,
key: key
});
});
return items;
}
public getLocalTime = (utcTime: string): string => {
return moment(utcTime).local().format(chartDateFormat);
}
public getFormattedDate = (datetime: string, format?: string): string => {
return moment(datetime).local().format(format ? format : defaultDateFormat);
}
public getQueryDateFormat = (datetime: string): string => {
return moment(datetime).local().format('YYYY-MM-DDT08:MM:00.000') + 'Z';
}
public getQueryStartDateFormat = (datetime: string): string => {
return moment(datetime).format('YYYY-MM-DDT00:00:00.000Z');
}
public getQueryEndDateFormat = (datetime: string): string => {
return moment(datetime).format('YYYY-MM-DDTHH:MM:00.000Z');
}
public getRandomColor = () => {
return "rgb(" + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + "," +
Math.floor(Math.random() * 255) + ")";
}
}

View File

@ -0,0 +1,33 @@
import * as React from 'react';
import styles from '../CommonControl.module.scss';
import { Pivot, PivotItem } from 'office-ui-fabric-react/lib/Pivot';
export interface IPivotProps {
ShowLabel: boolean;
LabelText: string;
SelectedKey: string;
Items: any[];
OnMenuClick: (item: PivotItem) => void;
}
const CustomPivot: React.FunctionComponent<IPivotProps> = (props) => {
return (
<div style={{ display: 'flex', paddingRight: '10px' }}>
{props.ShowLabel &&
<label className={styles.dataLabel}>{props.LabelText}</label>
}
<Pivot selectedKey={props.SelectedKey} onLinkClick={props.OnMenuClick} aria-label="Pivot" className={styles.pivotControl}>
{props.Items &&
props.Items.map(item => {
return (
<PivotItem headerText={item.text} itemKey={item.key} />
);
})
}
</Pivot>
</div>
);
};
export default CustomPivot;

View File

@ -0,0 +1,84 @@
import * as React from 'react';
import styles from '../CommonControl.module.scss';
import { DetailsList, IColumn, DetailsListLayoutMode, ConstrainMode, SelectionMode, IGroup } from 'office-ui-fabric-react/lib/DetailsList';
const groupBy: any = require('lodash/groupBy');
const findIndex: any = require('lodash/findIndex');
export interface IDataListProps {
Items: any[];
Columns: IColumn[];
GroupBy: boolean;
GroupByCol?: string;
CountCol?: string;
}
const DataList: React.FunctionComponent<IDataListProps> = (props) => {
const [columns, setColumns] = React.useState<IColumn[]>([]);
const [items, setItems] = React.useState<any[]>([]);
const [groups, setGroups] = React.useState<IGroup[]>([]);
const _getItemIndex = (key): number => {
return findIndex(props.Items, (o) => { return o.date == key; });
};
const _buildGroups = () => {
let grouped: any[] = groupBy(props.Items, props.GroupByCol);
let groupsTemp: IGroup[] = [];
Object.keys(grouped).map((key, index) => {
groupsTemp.push({
key: key,
name: key,
count: grouped[key].length,
startIndex: _getItemIndex(key)
});
});
setGroups(groupsTemp);
};
const _loadDataList = () => {
setColumns(props.Columns);
if (props.GroupBy && props.GroupByCol.length > 0 && props.CountCol.length > 0) _buildGroups();
setItems(props.Items);
};
React.useEffect(() => {
if (props.Items && props.Items.length > 0 && props.Columns && props.Columns.length > 0) {
_loadDataList();
}
}, [props.Items, props.Columns]);
return (
<div className={styles.dataList}>
{(groups.length > 0 && props.GroupBy && props.GroupByCol.length > 0 && props.CountCol.length > 0) ? (
<DetailsList
items={items}
setKey="set"
columns={columns}
compact={true}
groups={groups}
groupProps={{
showEmptyGroups: true
}}
layoutMode={DetailsListLayoutMode.justified}
constrainMode={ConstrainMode.unconstrained}
isHeaderVisible={true}
selectionMode={SelectionMode.none}
enableShimmer={true} />
) : (
<DetailsList
items={items}
setKey="set"
columns={columns}
compact={true}
layoutMode={DetailsListLayoutMode.justified}
constrainMode={ConstrainMode.unconstrained}
isHeaderVisible={true}
selectionMode={SelectionMode.none}
enableShimmer={true} />
)}
</div>
);
};
export default DataList;

View File

@ -0,0 +1,198 @@
import * as React from 'react';
import * as strings from 'AppInsightsDashboardWebPartStrings';
import styles from '../CommonControl.module.scss';
import { IconType, Icon } from 'office-ui-fabric-react/lib/Icon';
import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl';
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { PivotItem } from 'office-ui-fabric-react/lib/Pivot';
import { IColumn } from 'office-ui-fabric-react/lib/DetailsList';
import { css } from 'office-ui-fabric-react/lib/Utilities';
import { AppInsightsProps } from '../../webparts/appInsightsDashboard/components/AppInsightsDashboard';
import { TimeInterval, TimeSpan, Segments } from '../enumHelper';
import { IPageViewDetailProps, IPageViewCountProps } from '../CommonProps';
import SectionTitle from '../components/SectionTitle';
import CustomPivot from '../components/CustomPivot';
import DataList from '../components/DataList';
import Helper from '../Helper';
const map: any = require('lodash/map');
export interface IPageViewsProps {
helper: Helper;
}
const PageViews: React.FunctionComponent<IPageViewsProps> = (props) => {
const mainProps = React.useContext(AppInsightsProps);
const [loadingChart, setLoadingChart] = React.useState<boolean>(true);
const [loadingList, setLoadingList] = React.useState<boolean>(true);
const [noData, setNoData] = React.useState<boolean>(false);
const [timespanMenus, setTimeSpanMenus] = React.useState<any[]>([]);
const [timeintervalMenus, setTimeIntervalMenus] = React.useState<any[]>([]);
const [selTimeSpan, setSelTimeSpan] = React.useState<string>('');
const [selTimeInterval, setSelTimeInterval] = React.useState<string>('');
const [menuClick, setMenuClick] = React.useState<boolean>(false);
const [chartData, setChartData] = React.useState<any>(null);
const [chartOptions, setChartOptions] = React.useState<any>(null);
const [listCols, setListCols] = React.useState<IColumn[]>([]);
const [items, setItems] = React.useState<any[]>([]);
const _loadMenus = () => {
let tsMenus: any[] = props.helper.getTimeSpanMenu();
setTimeSpanMenus(tsMenus);
setSelTimeSpan(tsMenus[4].key);
let tiMenus: any[] = props.helper.getTimeIntervalMenu();
setTimeIntervalMenus(tiMenus);
setSelTimeInterval(tiMenus[3].key);
};
const handleTimeSpanMenuClick = (item: PivotItem) => {
setMenuClick(true);
setSelTimeSpan(item.props.itemKey);
};
const handleTimeIntervalMenuClick = (item: PivotItem) => {
setMenuClick(true);
setSelTimeInterval(item.props.itemKey);
};
const _loadPageViewsCount = async () => {
if (menuClick) setLoadingChart(true);
let response: IPageViewCountProps[] = await props.helper.getPageViewCount(TimeSpan[selTimeSpan], TimeInterval[selTimeInterval]);
if (response.length > 0) {
const data: Chart.ChartData = {
labels: map(response, 'date'),
datasets: [
{
label: 'Total Page Views',
fill: true,
lineTension: 0,
data: map(response, 'sum'),
backgroundColor: 'rgba(255, 159, 64, 0.2)',
borderColor: 'rgb(255, 159, 64)',
borderWidth: 1
}
]
};
setChartData(data);
const options: Chart.ChartOptions = {
legend: {
display: false
},
title: {
display: false,
text: "Page Views"
},
responsive: true,
animation: {
easing: 'easeInQuad'
}
};
setChartOptions(options);
setLoadingChart(false);
setMenuClick(false);
} else {
setLoadingChart(false);
setNoData(true);
setMenuClick(false);
}
};
const _generateColumns = () => {
let cols: IColumn[] = [];
cols.push({
key: 'Url', name: 'Url', fieldName: 'Url', minWidth: 100, maxWidth: 350,
onRender: (item: any, index: number, column: IColumn) => {
return (
<div className={styles.textWithIcon}>
<div className={styles.fileiconDiv}>
<Icon iconName="FileASPX" ariaLabel={item.Url} iconType={IconType.Default} />
</div>
{item.Url ? (
<a href={item.Url} target="_blank" className={styles.pageLink}>{item.Url}</a>
) : (
<span>{strings.Msg_NoUrl}</span>
)}
</div>
);
}
});
cols.push({
key: 'start', name: 'Start Date', fieldName: 'start', minWidth: 100, maxWidth: 150
});
cols.push({
key: 'end', name: 'End Date', fieldName: 'end', minWidth: 100, maxWidth: 150
});
cols.push({
key: 'count', name: '#PageViews', fieldName: 'count', minWidth: 100, maxWidth: 150
});
setListCols(cols);
};
const _loadPageViews = async () => {
if (menuClick) setLoadingList(true);
let response: IPageViewDetailProps[] = await props.helper.getPageViews(TimeSpan[selTimeSpan], TimeInterval[selTimeInterval], [Segments.PV_URL]);
if (response.length > 0) {
_generateColumns();
setItems(response);
setLoadingList(false);
setMenuClick(false);
} else {
setLoadingList(false);
setNoData(true);
setMenuClick(false);
}
};
React.useEffect(() => {
if (selTimeSpan && selTimeInterval) {
setNoData(false);
_loadPageViewsCount();
_loadPageViews();
}
}, [selTimeSpan, selTimeInterval]);
React.useEffect(() => {
if (props.helper) {
_loadMenus();
}
}, [mainProps.AppId, mainProps.AppKey, props.helper]);
return (
<div>
<SectionTitle Title={strings.SecTitle_PageViews} />
<div style={{ display: 'flex', padding: '5px' }}>
<div className={styles.centerDiv}>
<CustomPivot ShowLabel={true} LabelText={strings.Menu_TimeSpan} Items={timespanMenus} SelectedKey={selTimeSpan} OnMenuClick={handleTimeSpanMenuClick} />
<CustomPivot ShowLabel={true} LabelText={strings.Menu_TimeSpan} Items={timeintervalMenus} SelectedKey={selTimeInterval} OnMenuClick={handleTimeIntervalMenuClick} />
</div>
</div>
{!noData &&
<>
<div className={css("ms-Grid-row", styles.content)}>
<div className={"ms-Grid-col ms-xxxl6 ms-xxl6 ms-xl6 ms-lg6"}>
{(!loadingList && !noData) ? (
<DataList Items={items} Columns={listCols} GroupBy={true} GroupByCol={"date"} CountCol={"count"} />
) : (
<Spinner label={strings.Msg_LoadList} labelPosition={"bottom"} />
)}
</div>
<div className={css("ms-Grid-col ms-xxxl6 ms-xxl6 ms-xl6 ms-lg6", styles.chartContainer)}>
{(!loadingChart && !noData) ? (
<ChartControl
type={ChartType.Line}
data={chartData}
options={chartOptions}
className={styles.chart}
/>
) : (
<Spinner label={strings.Msg_LoadChart} labelPosition={"bottom"} />
)}
</div>
</div>
</>
}
{!loadingChart && !loadingList && noData &&
<MessageBar messageBarType={MessageBarType.error}>{strings.Msg_NoData}</MessageBar>
}
</div>
);
};
export default PageViews;

View File

@ -0,0 +1,233 @@
import * as React from 'react';
import * as strings from 'AppInsightsDashboardWebPartStrings';
import styles from '../CommonControl.module.scss';
import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl';
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
import { css } from 'office-ui-fabric-react/lib/Utilities';
import { DatePicker } from 'office-ui-fabric-react/lib/DatePicker';
import { addDays } from 'office-ui-fabric-react/lib/utilities/dateMath/DateMath';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { AppInsightsProps } from '../../webparts/appInsightsDashboard/components/AppInsightsDashboard';
import { IPerfDurationProps } from '../CommonProps';
import SectionTitle from '../components/SectionTitle';
import Helper from '../Helper';
const map: any = require('lodash/map');
const today: Date = new Date(Date.now());
const startMaxDate: Date = addDays(today, -1);
const minDate: Date = addDays(today, -90);
const maxDate: Date = new Date(Date.now());
export interface IPerformanceProps {
helper: Helper;
}
const PerformanceStatistics: React.FunctionComponent<IPerformanceProps> = (props) => {
const mainProps = React.useContext(AppInsightsProps);
const [menuClick, setMenuClick] = React.useState<boolean>(false);
const [message, setMessage] = React.useState<string>('');
const [loadingChart1, setLoadingChart1] = React.useState<boolean>(true);
const [noDataChart1, setNoDataChart1] = React.useState<boolean>(false);
const [startDate, setStartDate] = React.useState<Date>(null);
const [endDate, setEndDate] = React.useState<Date>(null);
const [chartData1, setChartData1] = React.useState<any>(null);
const [chartOptions1, setChartOptions1] = React.useState<any>(null);
const handleStartDateChange = (selDate: Date | null | undefined): void => {
setStartDate(selDate);
};
const handleEndDateChange = (selDate: Date | null | undefined): void => {
setEndDate(selDate);
};
const _loadOperationsDurations = async () => {
if (menuClick) setLoadingChart1(true);
let query: string = ``;
if (startDate && endDate) {
query += `let start=datetime("${props.helper.getQueryStartDateFormat(startDate.toUTCString())}");
let end=datetime("${props.helper.getQueryDateFormat(endDate.toUTCString())}");
let timeGrain=5m;
let dataset=pageViews
| where timestamp > start and timestamp < end
| where client_Type == "Browser" ;
dataset
| summarize count_=sum(itemCount), avg(duration), percentiles(duration, 50, 95, 99) by name
| union(dataset
| summarize count_=sum(itemCount), avg(duration), percentiles(duration, 50, 95, 99))
| order by count_ desc
`;
let response: any[] = await props.helper.getResponseByQuery(query, false);
if (response.length > 0) {
if (response[0][1] == 0 && response[0][2] == 'NaN') {
setMessage(strings.Msg_InvalidDate);
setNoDataChart1(true);
} else {
let finalRes: IPerfDurationProps[] = [];
response.map(res => {
finalRes.push({
PageName: res[0] ? res[0].indexOf('ModernDev -') >= 0 ? res[0].replace('ModernDev -', '') : res[0] : 'OverAll',
count: res[1],
AvgDuration: res[2],
PerDur_50: res[3],
PerDur_95: res[4],
PerDur_99: res[5]
});
});
const data: Chart.ChartData = {
labels: map(finalRes, 'PageName'),
datasets: [
{
label: 'Count',
fill: true,
lineTension: 0,
data: map(finalRes, 'count'),
backgroundColor: props.helper.getRandomColor()
},
{
label: 'Avg Duration',
fill: true,
lineTension: 0,
data: map(finalRes, 'AvgDuration'),
backgroundColor: props.helper.getRandomColor()
},
{
label: 'Percentile Duration 50',
fill: true,
lineTension: 0,
data: map(finalRes, 'PerDur_50'),
backgroundColor: props.helper.getRandomColor()
},
{
label: 'Percentile Duration 95',
fill: true,
lineTension: 0,
data: map(finalRes, 'PerDur_95'),
backgroundColor: props.helper.getRandomColor()
},
{
label: 'Percentile Duration 99',
fill: true,
lineTension: 0,
data: map(finalRes, 'PerDur_99'),
backgroundColor: props.helper.getRandomColor()
}
]
};
setChartData1(data);
const options: Chart.ChartOptions = {
legend: {
display: false
},
title: {
display: false,
text: ""
},
responsive: true,
animation: {
easing: 'easeInQuad'
},
scales:
{
xAxes: [{
stacked: true
}],
yAxes: [{
ticks: { beginAtZero: true },
stacked: true
}],
}
};
setChartOptions1(options);
}
} else {
setNoDataChart1(true);
}
} else {
setMessage(strings.Msg_NoDate);
setNoDataChart1(true);
}
setLoadingChart1(false);
setMenuClick(false);
};
React.useEffect(() => {
if (startDate && endDate) {
setMenuClick(true);
setNoDataChart1(false);
setMessage('');
_loadOperationsDurations();
}
}, [startDate, endDate]);
React.useEffect(() => {
if (props.helper) {
setStartDate(addDays(new Date(), -3));
setEndDate(today);
}
}, [mainProps.AppId, mainProps.AppKey, props.helper]);
return (
<div>
<SectionTitle Title={strings.SecTitle_PerfStats} />
<div style={{ display: 'flex', padding: '5px' }}>
<div className={styles.centerDiv}>
<div className={"ms-Grid-row"} style={{ display: 'inline-flex', marginTop: '-5px', marginLeft: '10px' }}>
<label className={styles.dataLabel} style={{ paddingTop: '5px' }}>{"Date Range: "}</label>
<div style={{ paddingRight: '5px' }}>
<DatePicker
isRequired={false}
placeholder="Start Date..."
ariaLabel="Select start date"
minDate={minDate}
maxDate={startMaxDate}
allowTextInput={false}
highlightSelectedMonth={true}
initialPickerDate={startMaxDate}
formatDate={(date?: Date) => { return props.helper.getFormattedDate(date.toUTCString()); }}
onSelectDate={handleStartDateChange}
value={startDate}
/>
</div>
<div>
<DatePicker
isRequired={false}
placeholder="End Date..."
ariaLabel="Select end date"
minDate={minDate}
maxDate={maxDate}
allowTextInput={false}
highlightSelectedMonth={true}
formatDate={(date?: Date) => { return props.helper.getFormattedDate(date.toUTCString()); }}
onSelectDate={handleEndDateChange}
value={endDate}
/>
</div>
</div>
</div>
</div>
<div className={css("ms-Grid-row", styles.content)}>
<div style={{ minHeight: '450px', maxHeight: '450px' }}>
{loadingChart1 ? (
<Spinner label={strings.Msg_LoadChart} labelPosition={"bottom"} />
) : (
<>
{!noDataChart1 ? (
<ChartControl
type={ChartType.Bar}
data={chartData1}
options={chartOptions1}
/>
) : (
<MessageBar messageBarType={MessageBarType.error}>{message ? message : strings.Msg_NoData}</MessageBar>
)}
</>
)}
</div>
</div>
</div>
);
};
export default PerformanceStatistics;

View File

@ -0,0 +1,16 @@
import * as React from 'react';
import styles from '../CommonControl.module.scss';
export interface ISectionTitleProps {
Title: string;
}
const SectionTitle: React.FunctionComponent<ISectionTitleProps> = (props) => {
return (
<div className={styles.secTitleContainer}>
<div className={styles.title}>{props.Title}</div>
</div>
);
};
export default SectionTitle;

View File

@ -0,0 +1,288 @@
import * as React from 'react';
import * as strings from 'AppInsightsDashboardWebPartStrings';
import styles from '../CommonControl.module.scss';
import { css } from 'office-ui-fabric-react/lib/Utilities';
import { PivotItem } from 'office-ui-fabric-react/lib/Pivot';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { DatePicker } from 'office-ui-fabric-react/lib/DatePicker';
import { addDays } from 'office-ui-fabric-react/lib/utilities/dateMath/DateMath';
import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl';
import { AppInsightsProps } from '../../webparts/appInsightsDashboard/components/AppInsightsDashboard';
import { TimeSpan, TimeInterval, Segments } from '../enumHelper';
import SectionTitle from '../components/SectionTitle';
import CustomPivot from './CustomPivot';
import Helper from '../Helper';
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
import { IColumn } from 'office-ui-fabric-react/lib/DetailsList';
import { Icon, IconType } from 'office-ui-fabric-react/lib/Icon';
import DataList from './DataList';
const map: any = require('lodash/map');
const today: Date = new Date(Date.now());
const startMaxDate: Date = addDays(today, -1);
const minDate: Date = addDays(today, -90);
const maxDate: Date = new Date(Date.now());
export interface IUserStatisticsProps {
helper: Helper;
}
const UserStatistics: React.FunctionComponent<IUserStatisticsProps> = (props) => {
const mainProps = React.useContext(AppInsightsProps);
const [loadingChart, setLoadingChart] = React.useState<boolean>(true);
const [loadingList, setLoadingList] = React.useState<boolean>(true);
const [timespanMenus, setTimeSpanMenus] = React.useState<any[]>([]);
const [selTimeSpan, setSelTimeSpan] = React.useState<string>('');
const [menuClick, setMenuClick] = React.useState<boolean>(false);
const [noData, setNoData] = React.useState<boolean>(false);
const [noListData, setNoListData] = React.useState<boolean>(false);
const [chartData, setChartData] = React.useState<any>(null);
const [chartOptions, setChartOptions] = React.useState<any>(null);
const [startDate, setStartDate] = React.useState<Date>(null);
const [endDate, setEndDate] = React.useState<Date>(null);
const [listCols, setListCols] = React.useState<IColumn[]>([]);
const [items, setItems] = React.useState<any[]>([]);
const _loadMenus = () => {
let tsMenus: any[] = props.helper.getTimeSpanMenu();
setTimeSpanMenus(tsMenus);
setSelTimeSpan(tsMenus[4].key);
};
const handleTimeSpanMenuClick = (item: PivotItem) => {
setMenuClick(true);
setSelTimeSpan(item.props.itemKey);
setStartDate(null);
setEndDate(null);
};
const handleStartDateChange = (selDate: Date | null | undefined): void => {
setStartDate(selDate);
};
const handleEndDateChange = (selDate: Date | null | undefined): void => {
setEndDate(selDate);
};
const _loadUserStatistics = async () => {
if (menuClick) setLoadingChart(true);
let query: string = ``;
if (startDate && endDate) {
query = `
union pageViews,customEvents
| where timestamp between(datetime("${props.helper.getQueryStartDateFormat(startDate.toUTCString())}")..datetime("${props.helper.getQueryDateFormat(endDate.toUTCString())}"))
| summarize Users=dcount(user_Id) by bin(timestamp, 1h)
| order by timestamp asc
`;
} else {
query = `
union pageViews,customEvents
| summarize Users=dcount(user_Id) by bin(timestamp, 1h)
| order by timestamp asc
`;
}
let response: any[] = await props.helper.getResponseByQuery(query, (startDate && endDate) ? false : true, TimeSpan[selTimeSpan]);
if (response.length > 0) {
let results: any[] = [];
response.map((res: any[]) => {
results.push({
oriDate: res[0],
date: props.helper.getLocalTime(res[0]),
sum: res[1]
});
});
const data: Chart.ChartData = {
labels: map(results, 'date'),
datasets: [
{
label: 'Total Users',
fill: true,
lineTension: 0,
data: map(results, 'sum'),
}
]
};
setChartData(data);
const options: Chart.ChartOptions = {
legend: {
display: false
},
title: {
display: false,
text: ""
},
responsive: true,
animation: {
easing: 'easeInQuad'
},
scales:
{
yAxes: [
{
ticks:
{
beginAtZero: true
}
}
]
}
};
setChartOptions(options);
setLoadingChart(false);
setMenuClick(false);
} else {
setLoadingChart(false);
setNoListData(true);
setMenuClick(false);
}
};
const _generateColumns = () => {
let cols: IColumn[] = [];
cols.push({
key: 'user', name: 'User', fieldName: 'user', minWidth: 100, maxWidth: 150,
onRender: (item: any, index: number, column: IColumn) => {
return (
<div className={styles.textWithIcon}>
{item.user ? (
<span>{item.user}</span>
) : (
<span>{strings.Msg_NoUser}</span>
)}
</div>
);
}
});
cols.push({
key: 'Url', name: 'Url', fieldName: 'Url', minWidth: 100, maxWidth: 350,
onRender: (item: any, index: number, column: IColumn) => {
return (
<div className={styles.textWithIcon}>
<div className={styles.fileiconDiv}>
<Icon iconName="FileASPX" ariaLabel={item.Url} iconType={IconType.Default} />
</div>
{item.Url ? (
<a href={item.Url} target="_blank" className={styles.pageLink}>{item.Url}</a>
) : (
<span>{strings.Msg_NoUrl}</span>
)}
</div>
);
}
});
cols.push({
key: 'count', name: 'View Count', fieldName: 'count', minWidth: 100, maxWidth: 150
});
setListCols(cols);
};
const _loadUsersPageViewsList = async () => {
if (menuClick) setLoadingList(true);
let response: any[] = [];
if (startDate && endDate) {
response = await props.helper.getUserPageViews(`${props.helper.getFormattedDate(startDate.toUTCString(), 'YYYY-MM-DD')}/${props.helper.getFormattedDate(addDays(endDate, 1).toUTCString(), 'YYYY-MM-DD')}`,
TimeInterval["1 Hour"], [Segments.Cust_UserTitle, Segments.PV_URL]);
} else {
response = await props.helper.getUserPageViews(TimeSpan[selTimeSpan], TimeInterval["1 Hour"], [Segments.Cust_UserTitle, Segments.PV_URL]);
}
if (response.length > 0) {
_generateColumns();
setItems(response);
setLoadingList(false);
setMenuClick(false);
} else {
setLoadingList(false);
setNoData(true);
setMenuClick(false);
}
};
React.useEffect(() => {
if (selTimeSpan || (startDate && endDate)) {
setNoData(false);
setNoListData(false);
_loadUserStatistics();
_loadUsersPageViewsList();
}
}, [selTimeSpan, startDate, endDate]);
React.useEffect(() => {
if (props.helper) {
_loadMenus();
}
}, [mainProps.AppId, mainProps.AppKey, props.helper]);
return (
<div>
<SectionTitle Title={strings.SecTitle_UserStats} />
<div style={{ display: 'flex', padding: '5px' }}>
<div className={styles.centerDiv}>
<CustomPivot ShowLabel={true} LabelText={strings.Menu_TimeSpan} Items={timespanMenus} SelectedKey={selTimeSpan} OnMenuClick={handleTimeSpanMenuClick} />
<div className={"ms-Grid-row"} style={{ display: 'inline-flex', marginTop: '-5px', marginLeft: '10px' }}>
<label className={styles.dataLabel} style={{ paddingTop: '5px' }}>{"Date Range: "}</label>
<div style={{ paddingRight: '5px' }}>
<DatePicker
isRequired={false}
placeholder="Start Date..."
ariaLabel="Select start date"
minDate={minDate}
maxDate={startMaxDate}
allowTextInput={false}
highlightSelectedMonth={true}
initialPickerDate={startMaxDate}
formatDate={(date?: Date) => { return props.helper.getFormattedDate(date.toUTCString()); }}
onSelectDate={handleStartDateChange}
value={startDate}
/>
</div>
<div>
<DatePicker
isRequired={false}
placeholder="End Date..."
ariaLabel="Select end date"
minDate={minDate}
maxDate={maxDate}
allowTextInput={false}
highlightSelectedMonth={true}
formatDate={(date?: Date) => { return props.helper.getFormattedDate(date.toUTCString()); }}
onSelectDate={handleEndDateChange}
value={endDate}
/>
</div>
</div>
</div>
</div>
<div className={css("ms-Grid-row", styles.content)}>
<div className={"ms-Grid-col ms-xxxl6 ms-xxl6 ms-xl6 ms-lg6"}>
{loadingList ? (
<Spinner label={strings.Msg_LoadList} labelPosition={"bottom"} />
) : (
<>
{!noListData ? (
<DataList Items={items} Columns={listCols} GroupBy={true} GroupByCol={"date"} CountCol={"count"} />
) : (
<MessageBar messageBarType={MessageBarType.error}>{strings.Msg_NoData}</MessageBar>
)}
</>
)}
</div>
<div className={"ms-Grid-col ms-xxxl6 ms-xxl6 ms-xl6 ms-lg6"} style={{ minHeight: '358px' }}>
{loadingChart ? (
<Spinner label={strings.Msg_LoadChart} labelPosition={"bottom"} />
) : (
<>
{!noData ? (
<ChartControl
type={ChartType.Bar}
data={chartData}
options={chartOptions}
className={styles.chart}
/>
) : (
<MessageBar messageBarType={MessageBarType.error}>{strings.Msg_NoData}</MessageBar>
)}
</>
)}
</div>
</div>
</div>
);
};
export default UserStatistics;

View File

@ -0,0 +1,28 @@
export enum TimeInterval {
"30 Min" = "PT30M",
"1 Hour" = "PT1H",
"3 Hour" = "PT3H",
"12 Hour" = "PT12H",
"1 Day" = "P1D",
"5 Day" = "P5D"
}
export enum TimeSpan {
"1 hour" = "PT1H",
"6 hours" = "PT6H",
"12 hours" = "PT12H",
"1 day" = "P1D",
"3 days" = "P3D",
"7 days" = "P7D",
"15 days" = "P15D",
"30 days" = "P30D",
"45 days" = "P45D",
"60 days" = "P60D",
"75 days" = "P75D",
"90 days" = "P90D",
}
export enum Segments {
"PV_URL" = "pageView/urlPath",
"PV_Name" = "pageView/name",
"OP_Name" = "operation/name",
"Cust_UserTitle" = "customDimensions/UserTitle"
}

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "968e1670-6570-4583-9164-686ced3bb63f",
"alias": "AppInsightsDashboardWebPart",
"componentType": "WebPart",
"version": "*",
"manifestVersion": 2,
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart","SharePointFullPage"],
"supportsFullBleed": true,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": { "default": "Other" },
"title": { "default": "AppInsights Dashboard" },
"description": { "default": "Displays the appinsights statistics." },
"officeFabricIconFontName": "BIDashboard",
"properties": { }
}]
}

View File

@ -0,0 +1,84 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'AppInsightsDashboardWebPartStrings';
import AppInsightsDashboard from './components/AppInsightsDashboard';
import { IAppInsightsDashboardProps } from './components/AppInsightsDashboard';
export interface IAppInsightsDashboardWebPartProps {
AppId: string;
AppKey: string;
}
export default class AppInsightsDashboardWebPart extends BaseClientSideWebPart<IAppInsightsDashboardWebPartProps> {
public render(): void {
const element: React.ReactElement<IAppInsightsDashboardProps> = React.createElement(
AppInsightsDashboard,
{
AppId: this.properties.AppId,
AppKey: this.properties.AppKey,
DisplayMode: this.displayMode,
onConfigure: this._onConfigure,
httpClient: this.context.httpClient
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
public _onConfigure = () => {
this.context.propertyPane.open();
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('AppId', {
label: strings.AppIdLabel,
multiline: false,
placeholder: strings.AppIdLabel,
resizable: false,
value: this.properties.AppId
}),
PropertyPaneTextField('AppKey', {
label: strings.AppKeyLabel,
multiline: false,
placeholder: strings.AppKeyLabel,
resizable: false,
value: this.properties.AppKey
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,13 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.appInsightsDashboard {
.container {
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
// background-color: $ms-color-themeDark;
padding: 20px;
}
}

View File

@ -0,0 +1,61 @@
import * as React from 'react';
import styles from './AppInsightsDashboard.module.scss';
import * as strings from 'AppInsightsDashboardWebPartStrings';
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import { DisplayMode } from '@microsoft/sp-core-library';
import { HttpClient } from '@microsoft/sp-http';
import PageViews from '../../../common/components/PageViews';
import UserStatistics from '../../../common/components/UserStatistics';
import PerformanceStatistics from '../../../common/components/PerformanceStatistics';
import Helper from '../../../common/Helper';
export interface IAppInsightsDashboardProps {
AppId: string;
AppKey: string;
DisplayMode: DisplayMode;
onConfigure: () => void;
httpClient: HttpClient;
}
export const AppInsightsProps = React.createContext<IAppInsightsDashboardProps>(null);
const AppInsightsDashboard: React.FunctionComponent<IAppInsightsDashboardProps> = (props) => {
const [helper, setHelper] = React.useState<any>(null);
React.useEffect(() => {
setHelper(new Helper(props.AppId, props.AppKey, props.httpClient));
}, [props.AppId, props.AppKey]);
return (
<AppInsightsProps.Provider value={props}>
<div className={styles.appInsightsDashboard}>
<div className={styles.container}>
{(!props.AppId || !props.AppKey) ? (
<Placeholder iconName='Edit'
iconText={strings.Config_IconText}
description={props.DisplayMode === DisplayMode.Edit ? strings.Config_Desc : strings.Config_Desc_ReadMode}
buttonLabel={strings.Config_ButtonText}
hideButton={props.DisplayMode === DisplayMode.Read}
onConfigure={props.onConfigure}
/>
) : (
<>
<div className={styles.row}>
<PageViews helper={helper} />
</div>
<div className={styles.row}>
<UserStatistics helper={helper} />
</div>
<div className={styles.row}>
<PerformanceStatistics helper={helper} />
</div>
</>
)}
</div>
</div>
</AppInsightsProps.Provider>
);
};
export default AppInsightsDashboard;

View File

@ -0,0 +1,28 @@
define([], function () {
return {
"PropertyPaneDescription": "",
"BasicGroupName": "",
AppIdLabel: "Application ID",
AppKeyLabel: "Application Key",
Config_IconText: 'App Insights Dashboard Configuration',
Config_Desc: 'Please configure the settings!!!',
Config_Desc_ReadMode: 'Please configure the settings. Edit the page to access the properties pane.',
Config_ButtonText: 'Configure',
Menu_TimeSpan: 'Show data for last:',
Menu_TimeInterval: 'Time Interval:',
SecTitle_PageViews: "Page Views Statistics",
SecTitle_UserStats: "Users Statistics",
SecTitle_PerfStats: "Performance Statistics",
Msg_NoData: 'Sorry no data!!!',
Msg_NoUrl: 'Sorry no "Url" captured!!!',
Msg_NoUser: 'Sorry, no user info!!!',
Msg_LoadList: 'Loading list, please wait...',
Msg_LoadChart: 'Loading chart, please wait...',
Msg_InvalidDate: 'Please choose the correct "Date Range" for results!!!',
Msg_NoDate: 'Please choose the "Date Range" for results!!!'
}
});

View File

@ -0,0 +1,31 @@
declare interface IAppInsightsDashboardWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
AppIdLabel: string;
AppKeyLabel: string;
Config_IconText: string;
Config_Desc: string;
Config_Desc_ReadMode: string;
Config_ButtonText: string;
Menu_TimeSpan: string;
Menu_TimeInterval: string;
SecTitle_PageViews: string;
SecTitle_UserStats: string;
SecTitle_PerfStats: string;
Msg_NoData: string;
Msg_NoUrl: string;
Msg_NoUser: string;
Msg_LoadList: string;
Msg_LoadChart: string;
Msg_InvalidDate: string;
Msg_NoDate: string;
}
declare module 'AppInsightsDashboardWebPartStrings' {
const strings: IAppInsightsDashboardWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}