add new application insights templates
This commit is contained in:
Родитель
ae3b060bde
Коммит
0a3620f54e
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 7.7 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 19 KiB |
|
@ -6,7 +6,7 @@ type IDict<T> = { [id: string]: T };
|
|||
type IDictionary = IDict<any>;
|
||||
type IStringDictionary = IDict<string>;
|
||||
|
||||
type IConnection = IStringDictionary;
|
||||
type IConnection = IStringDictionary;
|
||||
type IConnections = IDict<IConnection>;
|
||||
|
||||
/**
|
||||
|
@ -27,7 +27,7 @@ interface IDashboardConfig extends IDataSourceContainer, IElementsContainer {
|
|||
*/
|
||||
icon?: string,
|
||||
/**
|
||||
* An image logo to be diaplyed next to the title of the dashboard in the navigation header.
|
||||
* An image logo to be displayed next to the title of the dashboard in the navigation header.
|
||||
* Optional [Default: None]
|
||||
*/
|
||||
logo?: string,
|
||||
|
@ -51,13 +51,16 @@ interface IDashboardConfig extends IDataSourceContainer, IElementsContainer {
|
|||
* The category to put this template in the dashboard creation screen
|
||||
*/
|
||||
category?: string,
|
||||
|
||||
/**
|
||||
* A flag indicates whether the template is featured
|
||||
*/
|
||||
featured?: boolean,
|
||||
/**
|
||||
* Configuration relevant for the current dashboard
|
||||
*/
|
||||
config: {
|
||||
/**
|
||||
* A dictionary of connection paramters.
|
||||
* A dictionary of connection parameters.
|
||||
* connections: {
|
||||
* "application-insights": { appId: "123456", apiKey: "123456" },
|
||||
* "cosmos-db": { ... }
|
||||
|
@ -80,7 +83,7 @@ interface IDashboardConfig extends IDataSourceContainer, IElementsContainer {
|
|||
},
|
||||
filters: IFilter[]
|
||||
dialogs: IDialog[],
|
||||
layouts?: ILayouts
|
||||
layouts?: ILayouts,
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,10 +93,10 @@ interface IDashboardConfig extends IDataSourceContainer, IElementsContainer {
|
|||
*/
|
||||
|
||||
type DataSource = ConstantDataSource | AIDataSource | SampleDataSource;
|
||||
|
||||
/**
|
||||
* Data Source properties in a dashboard definition
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data Source properties in a dashboard definition
|
||||
*/
|
||||
interface IDataSource {
|
||||
/**
|
||||
* The name/type of the data source - should be in the data source `type` property
|
||||
|
@ -168,7 +171,7 @@ interface Sizes<T> {
|
|||
xxs?: T
|
||||
}
|
||||
|
||||
interface ILayout {
|
||||
interface ILayout {
|
||||
"i": string,
|
||||
"x": number,
|
||||
"y": number,
|
||||
|
@ -271,7 +274,7 @@ interface IElement {
|
|||
interface IFilter {
|
||||
type: string,
|
||||
source?: string,
|
||||
dependencies?: IStringDictionary,
|
||||
dependencies?: IStringDictionary,
|
||||
actions?: IStringDictionary,
|
||||
title?: string,
|
||||
subtitle?: string,
|
||||
|
@ -280,7 +283,7 @@ interface IFilter {
|
|||
}
|
||||
|
||||
interface IElementsContainer {
|
||||
elements: IElement[]
|
||||
elements: IElement[]
|
||||
}
|
||||
|
||||
interface IDialog extends IDataSourceContainer, IElementsContainer {
|
||||
|
|
|
@ -221,7 +221,7 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
onSubmitImport() {
|
||||
var dashboardId = this.state.fileName;
|
||||
ConfigurationsActions.submitDashboardFile(this.state.content, dashboardId);
|
||||
|
||||
|
||||
this.setState({ importVisible: false });
|
||||
}
|
||||
|
||||
|
@ -270,9 +270,9 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
|
||||
let createCard = (tmpl, index) => (
|
||||
<div key={index} className="md-cell" style={styles.card}>
|
||||
<Card
|
||||
className="md-block-centered"
|
||||
key={index}
|
||||
<Card
|
||||
className="md-block-centered"
|
||||
key={index}
|
||||
style={{ backgroundImage: `url(${tmpl.preview})`}} >
|
||||
<Media>
|
||||
<MediaOverlay>
|
||||
|
@ -280,24 +280,24 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
</MediaOverlay>
|
||||
</Media>
|
||||
<CardActions style={styles.fabs}>
|
||||
<Button
|
||||
floating
|
||||
secondary
|
||||
<Button
|
||||
floating
|
||||
secondary
|
||||
style={{ backgroundColor: '#959ba5', marginRight: '2px' }}
|
||||
onClick={this.onExportTemplate.bind(this, tmpl.id)}
|
||||
>
|
||||
file_download
|
||||
</Button>
|
||||
<Button
|
||||
floating
|
||||
secondary
|
||||
<Button
|
||||
floating
|
||||
secondary
|
||||
onClick={this.onOpenInfo.bind(this, tmpl.html || '<p>No info available</p>', tmpl.name)}
|
||||
>
|
||||
info
|
||||
</Button>
|
||||
<Button
|
||||
floating
|
||||
primary
|
||||
<Button
|
||||
floating
|
||||
primary
|
||||
onClick={this.onNewTemplateSelected.bind(this, tmpl.id)} style={styles.primaryFab}
|
||||
>
|
||||
add_circle_outline
|
||||
|
@ -308,22 +308,36 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
);
|
||||
|
||||
// Dividing templates into categories
|
||||
let categories = { 'General': [] };
|
||||
let categories = { 'General': [], 'Featured': [] };
|
||||
templates.forEach((tmpl, index) => {
|
||||
let category = tmpl.category || 'General';
|
||||
if (tmpl.featured) {
|
||||
categories['Featured'].push(createCard(tmpl, index));
|
||||
}
|
||||
categories[category] = categories[category] || [];
|
||||
categories[category].push(createCard(tmpl, index));
|
||||
});
|
||||
|
||||
// Sort templates alphabetically
|
||||
let sortedCategories = { 'General': categories.General, 'Featured': categories.Featured }
|
||||
const keys = Object.keys(categories).sort();
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let key = keys[i];
|
||||
if (key != 'Featured') {
|
||||
sortedCategories[key] = categories[key];
|
||||
}
|
||||
}
|
||||
categories = sortedCategories;
|
||||
|
||||
let toolbarActions = [];
|
||||
toolbarActions.push(
|
||||
(
|
||||
<Button
|
||||
flat
|
||||
tooltipLabel="Import dashboard"
|
||||
onClick={this.onOpenImport.bind(this)}
|
||||
label="Import dashboard"
|
||||
>file_upload
|
||||
flat
|
||||
tooltipLabel="Import dashboard"
|
||||
onClick={this.onOpenImport.bind(this)}
|
||||
label="Import dashboard"
|
||||
>file_upload
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
|
@ -346,31 +360,31 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
}
|
||||
|
||||
<Dialog
|
||||
id="ImportDashboard"
|
||||
visible={importVisible || false}
|
||||
title="Import dashboard"
|
||||
modal
|
||||
actions={[
|
||||
{ onClick: this.onCloseImport, primary: false, label: 'Cancel' },
|
||||
{ onClick: this.onSubmitImport, primary: true, label: 'Submit', disabled: !importedFileContent },
|
||||
]}>
|
||||
<FileUpload
|
||||
id="dashboardDefenitionFile"
|
||||
primary
|
||||
label="Choose File"
|
||||
accept="application/javascript"
|
||||
onLoadStart={this.setFile}
|
||||
onLoad={this.onLoad}
|
||||
/>
|
||||
<TextField
|
||||
id="dashboardFileName"
|
||||
label="Dashboard ID"
|
||||
value={fileName || ''}
|
||||
onChange={this.updateFileName}
|
||||
disabled={!importedFileContent}
|
||||
lineDirection="center"
|
||||
placeholder="Choose an ID for the imported dashboard"
|
||||
/>
|
||||
id="ImportDashboard"
|
||||
visible={importVisible || false}
|
||||
title="Import dashboard"
|
||||
modal
|
||||
actions={[
|
||||
{ onClick: this.onCloseImport, primary: false, label: 'Cancel' },
|
||||
{ onClick: this.onSubmitImport, primary: true, label: 'Submit', disabled: !importedFileContent },
|
||||
]}>
|
||||
<FileUpload
|
||||
id="dashboardDefenitionFile"
|
||||
primary
|
||||
label="Choose File"
|
||||
accept="application/javascript"
|
||||
onLoadStart={this.setFile}
|
||||
onLoad={this.onLoad}
|
||||
/>
|
||||
<TextField
|
||||
id="dashboardFileName"
|
||||
label="Dashboard ID"
|
||||
value={fileName || ''}
|
||||
onChange={this.updateFileName}
|
||||
disabled={!importedFileContent}
|
||||
lineDirection="center"
|
||||
placeholder="Choose an ID for the imported dashboard"
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
|
|
|
@ -45,16 +45,16 @@ import { IDataSourcePlugin } from '../../../data-sources/plugins/DataSourcePlugi
|
|||
* @param prevState The previous state to compare for changing filters
|
||||
*/
|
||||
export function bars(
|
||||
format: string | IDataFormat,
|
||||
state: any,
|
||||
dependencies: IDictionary,
|
||||
plugin: IDataSourcePlugin,
|
||||
format: string | IDataFormat,
|
||||
state: any,
|
||||
dependencies: IDictionary,
|
||||
plugin: IDataSourcePlugin,
|
||||
prevState: any) {
|
||||
|
||||
if (typeof format === 'string') {
|
||||
if (typeof format === 'string') {
|
||||
return formatWarn('format should be an object with args', 'bars', plugin);
|
||||
}
|
||||
|
||||
|
||||
const args = format.args || {};
|
||||
const prefix = getPrefix(format);
|
||||
const valueField = args.valueField || 'count';
|
||||
|
@ -93,8 +93,9 @@ export function bars(
|
|||
barValues[val[barsField]][othersValue] = (barValues[val[barsField]][othersValue] || 0) + val[valueField];
|
||||
series[othersValue] = true;
|
||||
} else {
|
||||
barValues[val[barsField]][val[seriesField]] = val[valueField];
|
||||
series[val[seriesField]] = true;
|
||||
let value = val[seriesField] || valueField;
|
||||
barValues[val[barsField]][value] = val[valueField];
|
||||
series[value] = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -102,7 +103,7 @@ export function bars(
|
|||
result[prefix + 'bar-values'] = _.values(barValues);
|
||||
|
||||
} else {
|
||||
result[prefix + 'bars'] = [ valueField ];
|
||||
result[prefix + 'bars'] = [valueField];
|
||||
result[prefix + 'bar-values'] = values;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import * as _ from 'lodash';
|
||||
import utils from '../../index';
|
||||
import { DataFormatTypes, IDataFormat, formatWarn, getPrefix } from '../common';
|
||||
import { IDataSourcePlugin } from '../../../data-sources/plugins/DataSourcePlugin';
|
||||
|
||||
/**
|
||||
* Formats a result to suite a pie chart
|
||||
*
|
||||
* Receives a list of filtering values:
|
||||
* values: [
|
||||
* { count: 10, field: 'piece 1' },
|
||||
* { count: 15, field: 'piece 2' },
|
||||
* { count: 44, field: 'piece 3' },
|
||||
* ]
|
||||
*
|
||||
* And outputs the result in a consumable filter way:
|
||||
* result: {
|
||||
* "prefix-pieData": [
|
||||
* { name: 'bar 1', value: 10},
|
||||
* { name: 'bar 2', value: 15},
|
||||
* { name: 'bar 3', value: 20},
|
||||
* ],
|
||||
* }
|
||||
*
|
||||
* "prefix-selected" will be able to hold the selected values from the filter component
|
||||
*
|
||||
* @param format {
|
||||
* type: 'pie',
|
||||
* args: {
|
||||
* value: string - The field name holding the value the pie piece
|
||||
* data: string - the state property holding the data (default is 'values').
|
||||
* label: string - The field name holding the series name (aggregation in a specific field)
|
||||
* maxLength: number - At what length to cut string values (default: 13),
|
||||
* }
|
||||
* }
|
||||
* @param state Current received state from data source
|
||||
* @param plugin The entire plugin (for id generation, params etc...)
|
||||
* @param prevState The previous state to compare for changing filters
|
||||
*/
|
||||
export function pie(
|
||||
format: string | IDataFormat,
|
||||
state: any,
|
||||
plugin: IDataSourcePlugin,
|
||||
prevState: any) {
|
||||
|
||||
if (typeof format === 'string') {
|
||||
return formatWarn('format should be an object with args', 'timeline', plugin);
|
||||
}
|
||||
|
||||
const args = format.args || {};
|
||||
let values: any[] = state[args.data || 'values'] || [];
|
||||
const prefix = getPrefix(format);
|
||||
|
||||
const labelField = args.label || 'name';
|
||||
const valueField = args.value || 'value';
|
||||
let maxLength = args.maxLength && parseInt(args.maxLength, 10) || 13;
|
||||
let maxLengthCut = Math.max(0, maxLength - 3);
|
||||
|
||||
let result = {};
|
||||
result[prefix + 'pieData'] = values.map(value => ({
|
||||
name: maxLength && value[labelField].length > maxLength
|
||||
? value[labelField].substr(0, maxLengthCut) + '...'
|
||||
: value[labelField],
|
||||
value: value[valueField]
|
||||
}));
|
||||
|
||||
return result;
|
||||
}
|
|
@ -69,7 +69,7 @@ export function timeline(
|
|||
|
||||
timeline.forEach(row => {
|
||||
let timestamp = row[timeField];
|
||||
let lineFieldValue = row[lineField];
|
||||
let lineFieldValue = lineField === undefined ? valueField : row[lineField];
|
||||
let valueFieldValue = row[valueField];
|
||||
|
||||
let timeValue = (new Date(timestamp)).getTime();
|
||||
|
|
|
@ -2,6 +2,7 @@ export * from './common';
|
|||
export * from './formats/bars';
|
||||
export * from './formats/filter';
|
||||
export * from './formats/flags';
|
||||
export * from './formats/pie';
|
||||
export * from './formats/retention';
|
||||
export * from './formats/scorecard';
|
||||
export * from './formats/timeline';
|
||||
|
|
|
@ -0,0 +1,505 @@
|
|||
/// <reference path="../../../client/@types/types.d.ts"/>
|
||||
import * as _ from 'lodash';
|
||||
|
||||
// The following line is important to keep in that format so it can be rendered into the page
|
||||
export const config: IDashboardConfig = /*return*/ {
|
||||
id: "app_insights_web",
|
||||
featured: true,
|
||||
name: "Application Insights Web Application",
|
||||
icon: "dashboard",
|
||||
url: "app_insights_web",
|
||||
description: "Dashboard to monitor web apps",
|
||||
preview: "/images/application-insights.png",
|
||||
category: "Web Apps",
|
||||
html: `<div>
|
||||
This is Application Insights dashboard for web apps.</br>
|
||||
This dashboard is built to view all data that is sent to Application Insights like requests,
|
||||
dependencies, exceptions, custom events etc..
|
||||
</div>`,
|
||||
config: {
|
||||
connections: {},
|
||||
layout: {
|
||||
isDraggable: true,
|
||||
isResizable: true,
|
||||
rowHeight: 30,
|
||||
verticalCompact: false,
|
||||
cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
|
||||
breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }
|
||||
}
|
||||
},
|
||||
dataSources: [
|
||||
{
|
||||
id: "timespan",
|
||||
type: "Constant",
|
||||
params: { values: ["24 hours", "1 week", "1 month"], selectedValue: "24 hours" },
|
||||
format: "timespan"
|
||||
},
|
||||
{
|
||||
id: "requests",
|
||||
type: "ApplicationInsights/Query",
|
||||
dependencies: { timespan: "timespan", queryTimespan: "timespan:queryTimespan", granularity: "timespan:granularity" },
|
||||
params: {
|
||||
table: "requests",
|
||||
queries: {
|
||||
duration: {
|
||||
query: ({ granularity }) => {
|
||||
return `
|
||||
where duration > 0 |
|
||||
summarize avg_duration= round(avg(duration) / 1000, 1) by bin(timestamp, ${granularity}) |
|
||||
order by timestamp asc `
|
||||
},
|
||||
format: { type: "timeline", args: { timeField: "timestamp", valueField: "avg_duration" } }
|
||||
},
|
||||
serverrequests: {
|
||||
query: ({ granularity }) => {
|
||||
return `
|
||||
summarize sum = sum(itemCount) by bin(timestamp, ${granularity}), success |
|
||||
order by timestamp asc `
|
||||
},
|
||||
format: {
|
||||
type: "bars",
|
||||
args: { barsField: "timestamp", seriesField: "success", valueField: "sum", threshold: 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "exceptions",
|
||||
type: "ApplicationInsights/Query",
|
||||
dependencies: { timespan: "timespan", queryTimespan: "timespan:queryTimespan", granularity: "timespan:granularity" },
|
||||
params: {
|
||||
table: "exceptions",
|
||||
queries: {
|
||||
type: {
|
||||
query: ({ granularity }) => {
|
||||
return `summarize count = count() by type `
|
||||
},
|
||||
format: { type: "pie", args: { value: "count", label: "type", maxLength: 20 } }
|
||||
},
|
||||
mapActivity: {
|
||||
query: () => `
|
||||
extend location=strcat(client_City, ', ', client_CountryOrRegion)
|
||||
| summarize location_count=count() by location
|
||||
| extend popup=strcat('<b>', location, '</b><br />', location_count, ' exceptions')`
|
||||
},
|
||||
count: {
|
||||
query: () =>
|
||||
` summarize count = count() `,
|
||||
format: {
|
||||
type: "scorecard",
|
||||
args: {
|
||||
countField: "count",
|
||||
thresholds: [{ value: 0, color: "#2196F3", icon: "bug_report", heading: "Exceptions" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "customEvents",
|
||||
type: "ApplicationInsights/Query",
|
||||
dependencies: { timespan: "timespan", queryTimespan: "timespan:queryTimespan", granularity: "timespan:granularity" },
|
||||
params: {
|
||||
table: "customEvents",
|
||||
queries: {
|
||||
usercount: {
|
||||
query: () =>
|
||||
`
|
||||
summarize count = dcount(user_Id) `,
|
||||
format: {
|
||||
type: "scorecard",
|
||||
args: {
|
||||
countField: "count",
|
||||
thresholds: [{ value: 0, color: "#2196F3", icon: "account_circle", heading: "Users" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "traces",
|
||||
type: "ApplicationInsights/Query",
|
||||
dependencies: { timespan: "timespan", queryTimespan: "timespan:queryTimespan", granularity: "timespan:granularity" },
|
||||
params: {
|
||||
table: "traces",
|
||||
queries: {
|
||||
count: {
|
||||
query: () =>
|
||||
` summarize count = count() `,
|
||||
format: {
|
||||
type: "scorecard",
|
||||
args: {
|
||||
countField: "count",
|
||||
thresholds: [{ value: 0, color: "#2196F3", icon: "format_align_justify", heading: "Traces" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
top: {
|
||||
query: () =>
|
||||
` project timestamp , message |
|
||||
order by timestamp desc |
|
||||
take 10 `
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "dependencies",
|
||||
type: "ApplicationInsights/Query",
|
||||
dependencies: { timespan: "timespan", queryTimespan: "timespan:queryTimespan", granularity: "timespan:granularity" },
|
||||
params: {
|
||||
table: "dependencies",
|
||||
queries: {
|
||||
sum: {
|
||||
query: ({ granularity }) => {
|
||||
return `
|
||||
summarize sum = sum(itemCount) by bin(timestamp, ${granularity}), name |
|
||||
order by timestamp asc `
|
||||
},
|
||||
format: {
|
||||
type: "bars",
|
||||
args: { barsField: "timestamp", seriesField: "name", valueField: "sum", threshold: 1 }
|
||||
}
|
||||
},
|
||||
count: {
|
||||
query: () =>
|
||||
` summarize count = count() `,
|
||||
format: {
|
||||
type: "scorecard",
|
||||
args: {
|
||||
countField: "count",
|
||||
thresholds: [{ value: 0, color: "#2196F3", icon: "input", heading: "Dependencies" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
top: {
|
||||
query: () =>
|
||||
` project timestamp , target, data |
|
||||
order by timestamp desc |
|
||||
take 10 `
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "retention",
|
||||
type: "ApplicationInsights/Query",
|
||||
dependencies: { timespan: "timespan", selectedTimespan: "timespan:queryTimespan", queryTimespan: "::P90D" },
|
||||
format: "retention",
|
||||
params: {
|
||||
query: () => `
|
||||
customEvents |
|
||||
summarize oldestVisit=min(timestamp), lastVisit=max(timestamp) by user_Id |
|
||||
summarize
|
||||
totalUnique = dcount(user_Id),
|
||||
totalUniqueUsersIn24hr = countif(lastVisit > ago(24hr)),
|
||||
totalUniqueUsersIn7d = countif(lastVisit > ago(7d)),
|
||||
totalUniqueUsersIn30d = countif(lastVisit > ago(30d)),
|
||||
returning24hr = countif(lastVisit > ago(24hr) and oldestVisit <= ago(24hr)),
|
||||
returning7d = countif(lastVisit > ago(7d) and oldestVisit <= ago(7d)),
|
||||
returning30d = countif(lastVisit > ago(30d) and oldestVisit <= ago(30d))
|
||||
`
|
||||
}
|
||||
}
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
type: "TextFilter",
|
||||
title: "Timespan",
|
||||
source: "timespan",
|
||||
actions: { onChange: "timespan:updateSelectedValue" },
|
||||
first: true
|
||||
}
|
||||
],
|
||||
elements: [
|
||||
{
|
||||
id: "serverResponseTime",
|
||||
type: "Timeline",
|
||||
title: "Server response time (sec)",
|
||||
subtitle: "Time between receiving HTTP request and finishing sending the response",
|
||||
size: { w: 4, h: 8 },
|
||||
source: "requests:duration"
|
||||
},
|
||||
{
|
||||
id: "serverRequests",
|
||||
type: "BarData",
|
||||
title: "Server requests by success",
|
||||
subtitle: "Requests count",
|
||||
size: { w: 4, h: 8 },
|
||||
source: "requests:serverrequests"
|
||||
},
|
||||
{
|
||||
id: "scores",
|
||||
type: "Scorecard",
|
||||
size: { w: 4, h: 3 },
|
||||
source: {
|
||||
users: "customEvents:usercount",
|
||||
traces: "traces:count",
|
||||
dependencies: "dependencies:count",
|
||||
exceptions: "exceptions:count"
|
||||
},
|
||||
dependencies: {
|
||||
card_users_onClick: "::onUsers",
|
||||
card_traces_onClick: "::onTraces",
|
||||
card_dependencies_onClick: "::onDependencies",
|
||||
card_exceptions_onClick: "::onExceptions"
|
||||
},
|
||||
actions: {
|
||||
onUsers: {
|
||||
action: "dialog:userRetention",
|
||||
params: { title: "args:heading", queryspan: "timespan:queryTimespan" }
|
||||
},
|
||||
onTraces: { action: "dialog:tracesview", params: { title: "args:heading", queryspan: "timespan:queryTimespan" } },
|
||||
onDependencies: {
|
||||
action: "dialog:dependenciesview",
|
||||
params: { title: "args:heading", queryspan: "timespan:queryTimespan" }
|
||||
},
|
||||
onExceptions: {
|
||||
action: "dialog:errors",
|
||||
params: {
|
||||
title: "args:heading",
|
||||
type: "args:type",
|
||||
innermostMessage: "args:innermostMessage",
|
||||
queryspan: "timespan:queryTimespan"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "exceptionType",
|
||||
type: "PieData",
|
||||
title: "Exception Type",
|
||||
subtitle: "Exceptions type",
|
||||
size: { w: 4, h: 8 },
|
||||
source: "exceptions:type",
|
||||
props: { entityType: "Type" }
|
||||
},
|
||||
{
|
||||
id: "dependenciescount",
|
||||
type: "BarData",
|
||||
title: "Dependencies",
|
||||
subtitle: "Dependencies sum by type",
|
||||
size: { w: 4, h: 8 },
|
||||
source: "dependencies:sum"
|
||||
},
|
||||
{
|
||||
id: "map",
|
||||
type: "MapData",
|
||||
title: "Exceptions Map",
|
||||
subtitle: "Monitor regional activity",
|
||||
size: { w: 4, h: 13 },
|
||||
location: { x: 9, y: 1 },
|
||||
source: "exceptions:mapActivity",
|
||||
props: { mapProps: { zoom: 1, maxZoom: 6 }, searchLocations: true }
|
||||
}
|
||||
],
|
||||
dialogs: [
|
||||
{
|
||||
id: "userRetention",
|
||||
width: "50%",
|
||||
params: ["title", "queryspan"],
|
||||
dataSources: [
|
||||
{
|
||||
id: "ai-ur",
|
||||
type: "ApplicationInsights/Query",
|
||||
dependencies: { timespan: "timespan", queryTimespan: "timespan:queryTimespan", granularity: "timespan:granularity" },
|
||||
params: {
|
||||
table: "customEvents",
|
||||
queries: {
|
||||
retention_top_users: {
|
||||
query: () => `
|
||||
summarize user_count=count() by user_Id |
|
||||
top 5 by user_count
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
elements: [
|
||||
{
|
||||
id: "user-retention-table",
|
||||
type: "Table",
|
||||
title: "User Retention",
|
||||
size: { w: 3, h: 9 },
|
||||
dependencies: { values: "retention" },
|
||||
props: {
|
||||
compact: true,
|
||||
cols: [
|
||||
{ header: "Time Span", field: "timespan" },
|
||||
{ header: "Retention", field: "retention" },
|
||||
{ header: "Returning", field: "returning" },
|
||||
{ header: "Unique Users", field: "unique" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "top-users-table",
|
||||
type: "Table",
|
||||
title: "Top Users",
|
||||
size: { w: 3, h: 9 },
|
||||
dependencies: { values: "ai-ur:retention_top_users" },
|
||||
props: {
|
||||
compact: true,
|
||||
cols: [{ header: "User Id", field: "user_Id" }, { header: "Count", field: "user_count" }]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "tracesview",
|
||||
dataSources: [],
|
||||
params: [],
|
||||
elements: [
|
||||
{
|
||||
id: "top-traces",
|
||||
type: "Table",
|
||||
title: "Latests traces",
|
||||
size: { w: 12, h: 9 },
|
||||
dependencies: { values: "traces:top" },
|
||||
props: {
|
||||
compact: true,
|
||||
cols: [{ header: "Timestamp", width: "10px", field: "timestamp" }, { header: "Message", field: "message" }]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "dependenciesview",
|
||||
dataSources: [],
|
||||
params: [],
|
||||
elements: [
|
||||
{
|
||||
id: "top-dependencies",
|
||||
type: "Table",
|
||||
title: "Latests dependencies",
|
||||
size: { w: 12, h: 9 },
|
||||
dependencies: { values: "dependencies:top" },
|
||||
props: {
|
||||
compact: true,
|
||||
cols: [
|
||||
{ header: "Timestamp", width: "10px", field: "timestamp" },
|
||||
{ header: "Target", width: "10px", field: "target" },
|
||||
{ header: "Data", field: "data" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "errors",
|
||||
width: "90%",
|
||||
params: ["title", "queryspan"],
|
||||
dataSources: [
|
||||
{
|
||||
id: "errors-group",
|
||||
type: "ApplicationInsights/Query",
|
||||
dependencies: { queryTimespan: "dialog_errors:queryspan" },
|
||||
params: {
|
||||
query: () => `
|
||||
exceptions
|
||||
| summarize error_count=count() by type, innermostMessage
|
||||
| project type, innermostMessage, error_count
|
||||
| order by error_count desc `
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "errors-selection",
|
||||
type: "ApplicationInsights/Query",
|
||||
dependencies: {
|
||||
queryTimespan: "dialog_errors:queryspan",
|
||||
type: "args:type",
|
||||
innermostMessage: "args:innermostMessage"
|
||||
},
|
||||
params: {
|
||||
query: ({ type, innermostMessage }) => `
|
||||
exceptions
|
||||
| where type == '${type}'
|
||||
| where innermostMessage == "${innermostMessage}"
|
||||
| project type, innermostMessage, handledAt, operation_Id `
|
||||
}
|
||||
}
|
||||
],
|
||||
elements: [
|
||||
{
|
||||
id: "errors-list",
|
||||
type: "SplitPanel",
|
||||
title: "Errors",
|
||||
size: { w: 12, h: 16 },
|
||||
dependencies: { groups: "errors-group", values: "errors-selection" },
|
||||
props: {
|
||||
cols: [
|
||||
{ header: "Type", field: "type", secondaryHeader: "Message", secondaryField: "innermostMessage" },
|
||||
{ header: "Operation Id", field: "operation_Id" },
|
||||
{ header: "HandledAt", field: "handledAt" },
|
||||
{ type: "button", value: "more", click: "openErrorDetail" }
|
||||
],
|
||||
group: { field: "type", secondaryField: "innermostMessage", countField: "error_count" }
|
||||
},
|
||||
actions: {
|
||||
select: {
|
||||
action: "errors-selection:updateDependencies",
|
||||
params: {
|
||||
title: "args:type",
|
||||
type: "args:type",
|
||||
innermostMessage: "args:innermostMessage",
|
||||
queryspan: "timespan:queryTimespan"
|
||||
}
|
||||
},
|
||||
openErrorDetail: {
|
||||
action: "dialog:errordetail",
|
||||
params: {
|
||||
title: "args:operation_Id",
|
||||
type: "args:type",
|
||||
innermostMessage: "args:innermostMessage",
|
||||
handledAt: "args:handledAt",
|
||||
operation_Id: "args:operation_Id",
|
||||
queryspan: "timespan:queryTimespan"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "errordetail",
|
||||
width: "50%",
|
||||
params: ["title", "handledAt", "type", "operation_Id", "queryspan"],
|
||||
dataSources: [
|
||||
{
|
||||
id: "errordetail-data",
|
||||
type: "ApplicationInsights/Query",
|
||||
dependencies: { operation_Id: "dialog_errordetail:operation_Id", queryTimespan: "dialog_errordetail:queryspan" },
|
||||
params: {
|
||||
query: ({ operation_Id }) => `
|
||||
exceptions
|
||||
| where operation_Id == '${operation_Id}'
|
||||
| project handledAt, type, innermostMessage, operation_Id, timestamp, details `
|
||||
}
|
||||
}
|
||||
],
|
||||
elements: [
|
||||
{
|
||||
id: "errordetail-item",
|
||||
type: "Detail",
|
||||
title: "Error detail",
|
||||
size: { w: 12, h: 16 },
|
||||
dependencies: { values: "errordetail-data" },
|
||||
props: {
|
||||
cols: [
|
||||
{ header: "Handle", field: "handledAt" },
|
||||
{ header: "Type", field: "type" },
|
||||
{ header: "Message", field: "innermostMessage" },
|
||||
{ header: "Operation ID", field: "operation_Id" },
|
||||
{ header: "Timestamp", field: "timestamp" },
|
||||
{ header: "Details", field: "details" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/// <reference path="../../../client/@types/types.d.ts"/>
|
||||
import * as _ from 'lodash';
|
||||
|
||||
// The following line is important to keep in that format so it can be rendered into the page
|
||||
export const config: IDashboardConfig = /*return*/ {
|
||||
id: "app_insights_sample",
|
||||
name: "Application Insights Sample",
|
||||
icon: "dashboard",
|
||||
url: "app_insights_sample",
|
||||
description: "A basic Application Insights dashboard",
|
||||
preview: "/images/sample.png",
|
||||
category: 'Samples',
|
||||
html: `
|
||||
<div>
|
||||
This is a basic Application Insights dashboard. </br>
|
||||
This dashboard is built to view Requests that are sent to Application Insights.
|
||||
</div>
|
||||
`,
|
||||
config: {
|
||||
connections: {},
|
||||
layout: {
|
||||
isDraggable: true,
|
||||
isResizable: true,
|
||||
rowHeight: 30,
|
||||
verticalCompact: false,
|
||||
cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
|
||||
breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }
|
||||
}
|
||||
},
|
||||
dataSources: [
|
||||
{
|
||||
id: "timespan",
|
||||
type: "Constant",
|
||||
params: { values: ["24 hours", "1 week", "1 month"], selectedValue: "24 hours" },
|
||||
format: "timespan"
|
||||
},
|
||||
{
|
||||
id: "requests",
|
||||
type: "ApplicationInsights/Query",
|
||||
dependencies: { timespan: "timespan", queryTimespan: "timespan:queryTimespan", granularity: "timespan:granularity" },
|
||||
params: {
|
||||
table: "requests",
|
||||
queries: {
|
||||
count: {
|
||||
query: ({ granularity }) => {
|
||||
return `
|
||||
summarize count= count() by bin(timestamp, ${granularity}) |
|
||||
order by timestamp asc `
|
||||
},
|
||||
format: { type: "timeline", args: { timeField: "timestamp", valueField: "count" } }
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
type: "TextFilter",
|
||||
title: "Timespan",
|
||||
source: "timespan",
|
||||
actions: { onChange: "timespan:updateSelectedValue" },
|
||||
first: true
|
||||
},
|
||||
],
|
||||
elements: [
|
||||
{
|
||||
id: "serverResponseTime",
|
||||
type: "Timeline",
|
||||
title: "Server requests count",
|
||||
subtitle: "The server requests count in the selected time range",
|
||||
size: { w: 5, h: 8 },
|
||||
source: "requests:count",
|
||||
},
|
||||
],
|
||||
dialogs: [
|
||||
]
|
||||
}
|
|
@ -4,12 +4,13 @@ import * as _ from 'lodash';
|
|||
/* tslint:disable:indent quotemark max-line-length */
|
||||
// The following line is important to keep in that format so it can be rendered into the page
|
||||
export const config: IDashboardConfig = /*return*/ {
|
||||
id: "bot_analytics_inst",
|
||||
id: "bot_analytics_inst",
|
||||
featured: true,
|
||||
name: "Bot Analytics Instrumented Dashboard",
|
||||
icon: "dashboard",
|
||||
url: "bot_analytics_inst",
|
||||
description: "Microsoft Bot Framework based analytics",
|
||||
preview: "/images/bot-ai-cs.png",
|
||||
preview: "/images/bot-instrumented.png",
|
||||
category: 'Bots',
|
||||
html: `
|
||||
<div>
|
||||
|
|
|
@ -23,13 +23,18 @@ const fields = {
|
|||
url: /\s*url:\s*("|')(.*)("|')/,
|
||||
preview: /\s*preview:\s*("|')(.*)("|')/,
|
||||
category: /\s*category:\s*("|')(.*)("|')/,
|
||||
html: /\s*html:\s*(`)([\s\S]*?)(`)/gm
|
||||
html: /\s*html:\s*(`)([\s\S]*?)(`)/gm,
|
||||
featured: /\s*featured:\s*(true|false)/,
|
||||
}
|
||||
|
||||
const getField = (regExp, text) => {
|
||||
regExp.lastIndex = 0;
|
||||
const matches = regExp.exec(text);
|
||||
return matches && matches.length >= 3 && matches[2];
|
||||
let match = matches && matches.length >= 3 && matches[2];
|
||||
if (!match) {
|
||||
match = matches && matches.length > 0 && matches[0]
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
const getMetadata = (text) => {
|
||||
|
@ -60,7 +65,7 @@ const getFileContents = (filePath) => {
|
|||
|
||||
const ensureCustomTemplatesFolderExists = () => {
|
||||
const { privateTemplate } = paths();
|
||||
|
||||
|
||||
if (!fs.existsSync(privateTemplate)) {
|
||||
fs.mkdirSync(privateTemplate);
|
||||
}
|
||||
|
@ -69,13 +74,13 @@ const ensureCustomTemplatesFolderExists = () => {
|
|||
router.get('/dashboards', (req, res) => {
|
||||
|
||||
const { privateDashboard, preconfDashboard, privateTemplate } = paths();
|
||||
|
||||
|
||||
let script = '';
|
||||
let files = fs.readdirSync(privateDashboard);
|
||||
files = (files || []).filter(fileName => isValidFile(path.join(privateDashboard, fileName)));
|
||||
|
||||
// In case no dashboard is present, create a new sample file
|
||||
if (!files || !files.length) {
|
||||
if (!files || !files.length) {
|
||||
const sampleFileName = 'basic_sample.private.js';
|
||||
const sampleTemplatePath = path.join(preconfDashboard, 'sample.ts');
|
||||
const samplePath = path.join(privateDashboard, sampleFileName);
|
||||
|
@ -85,7 +90,7 @@ router.get('/dashboards', (req, res) => {
|
|||
files = [ sampleFileName ];
|
||||
}
|
||||
|
||||
if (files && files.length) {
|
||||
if (files && files.length) {
|
||||
files.forEach((fileName) => {
|
||||
const filePath = path.join(privateDashboard, fileName);
|
||||
const fileContents = getFileContents(filePath);
|
||||
|
@ -114,7 +119,7 @@ router.get('/dashboards', (req, res) => {
|
|||
const fileContents = getFileContents(filePath);
|
||||
const jsonDefinition = getMetadata(fileContents);
|
||||
let content = 'return ' + JSON.stringify(jsonDefinition);
|
||||
|
||||
|
||||
// Ensuing this dashboard is loaded into the dashboards array on the page
|
||||
script += `
|
||||
(function (window) {
|
||||
|
@ -138,7 +143,7 @@ router.get('/dashboards', (req, res) => {
|
|||
const fileContents = getFileContents(filePath);
|
||||
const jsonDefinition = getMetadata(fileContents);
|
||||
let content = 'return ' + JSON.stringify(jsonDefinition);
|
||||
|
||||
|
||||
// Ensuing this dashboard is loaded into the dashboards array on the page
|
||||
script += `
|
||||
(function (window) {
|
||||
|
@ -152,8 +157,8 @@ router.get('/dashboards', (req, res) => {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.send(script);
|
||||
|
||||
res.send(script);
|
||||
});
|
||||
|
||||
router.get('/dashboards/:id*', (req, res) => {
|
||||
|
@ -184,13 +189,13 @@ router.get('/dashboards/:id*', (req, res) => {
|
|||
}
|
||||
}
|
||||
|
||||
res.send(script);
|
||||
res.send(script);
|
||||
});
|
||||
|
||||
router.post('/dashboards/:id', (req, res) => {
|
||||
let { id } = req.params;
|
||||
let { script } = req.body || '';
|
||||
|
||||
|
||||
const { privateDashboard } = paths();
|
||||
let dashboardFile = getFileById(privateDashboard, id);
|
||||
let filePath = path.join(privateDashboard, dashboardFile);
|
||||
|
@ -211,9 +216,9 @@ router.get('/templates/:id', (req, res) => {
|
|||
let templatePath = path.join(__dirname, '..', 'dashboards', 'preconfigured');
|
||||
|
||||
let script = '';
|
||||
|
||||
|
||||
let templateFile = getFileById(templatePath, templateId);
|
||||
|
||||
|
||||
if (!templateFile) {
|
||||
//fallback to custom template
|
||||
templatePath = path.join(__dirname, '..', 'dashboards', 'customTemplates');
|
||||
|
@ -236,7 +241,7 @@ router.get('/templates/:id', (req, res) => {
|
|||
}
|
||||
}
|
||||
|
||||
res.send(script);
|
||||
res.send(script);
|
||||
});
|
||||
|
||||
router.put('/templates/:id', (req, res) => {
|
||||
|
@ -324,14 +329,14 @@ router.delete('/dashboards/:id', (req, res) => {
|
|||
|
||||
function getFileById(dir, id, overwrite) {
|
||||
let files = fs.readdirSync(dir) || [];
|
||||
|
||||
|
||||
// Make sure the array only contains files
|
||||
files = files.filter(fileName => fs.statSync(path.join(dir, fileName)).isFile());
|
||||
if (!files || files.length === 0) {
|
||||
if (!files || files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
let dashboardFile = null;
|
||||
files.every(fileName => {
|
||||
const filePath = path.join(dir, fileName);
|
||||
|
@ -363,7 +368,7 @@ function getFileById(dir, id, overwrite) {
|
|||
|
||||
function isAuthorizedToSetup(req) {
|
||||
if (!fs.existsSync(privateSetupPath)) { return true; }
|
||||
|
||||
|
||||
let configString = fs.readFileSync(privateSetupPath, 'utf8');
|
||||
let config = JSON.parse(configString);
|
||||
|
||||
|
@ -378,7 +383,7 @@ function isAuthorizedToSetup(req) {
|
|||
|
||||
router.get('/setup', (req, res) => {
|
||||
|
||||
if (!isAuthorizedToSetup(req)) {
|
||||
if (!isAuthorizedToSetup(req)) {
|
||||
return res.send({ error: new Error('User is not authorized to setup') });
|
||||
}
|
||||
|
||||
|
@ -393,7 +398,7 @@ router.get('/setup', (req, res) => {
|
|||
|
||||
router.post('/setup', (req, res) => {
|
||||
|
||||
if (!isAuthorizedToSetup(req)) {
|
||||
if (!isAuthorizedToSetup(req)) {
|
||||
return res.send({ error: new Error('User is not authorized to setup') });
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче