Merge pull request #1280 from sudharsank/react-appinsights-dashboard
25
samples/react-appinsights-dashboard/.editorconfig
Normal 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
|
32
samples/react-appinsights-dashboard/.gitignore
vendored
Normal 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
|
12
samples/react-appinsights-dashboard/.yo-rc.json
Normal 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"
|
||||
}
|
||||
}
|
64
samples/react-appinsights-dashboard/README.md
Normal 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
|
||||
|
||||

|
||||

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

|
||||
#### Page Statistics
|
||||

|
||||
#### User Statistics
|
||||

|
||||
#### Performance Statistics
|
||||

|
||||
|
||||
## 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.
|
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 22 MiB |
BIN
samples/react-appinsights-dashboard/assets/PageStatistics.png
Normal file
After Width: | Height: | Size: 75 KiB |
After Width: | Height: | Size: 31 KiB |
BIN
samples/react-appinsights-dashboard/assets/UserStatistics.png
Normal file
After Width: | Height: | Size: 70 KiB |
19
samples/react-appinsights-dashboard/config/config.json
Normal 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"
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
@ -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 -->"
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
10
samples/react-appinsights-dashboard/config/serve.json
Normal 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/"
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
7
samples/react-appinsights-dashboard/gulpfile.js
vendored
Normal 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'));
|
17173
samples/react-appinsights-dashboard/package-lock.json
generated
Normal file
45
samples/react-appinsights-dashboard/package.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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%;
|
||||
}
|
@ -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;
|
||||
}
|
164
samples/react-appinsights-dashboard/src/common/Helper.ts
Normal 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) + ")";
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
28
samples/react-appinsights-dashboard/src/common/enumHelper.ts
Normal 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"
|
||||
}
|
1
samples/react-appinsights-dashboard/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
@ -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": { }
|
||||
}]
|
||||
}
|
@ -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
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
28
samples/react-appinsights-dashboard/src/webparts/appInsightsDashboard/loc/en-us.js
vendored
Normal 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!!!'
|
||||
}
|
||||
});
|
31
samples/react-appinsights-dashboard/src/webparts/appInsightsDashboard/loc/mystrings.d.ts
vendored
Normal 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;
|
||||
}
|
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 1.4 KiB |
38
samples/react-appinsights-dashboard/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
30
samples/react-appinsights-dashboard/tslint.json
Normal 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
|
||||
}
|
||||
}
|