add new application insights templates

This commit is contained in:
Ilana Kantorov 2017-08-21 15:46:57 +03:00
Родитель ae3b060bde
Коммит 0a3620f54e
12 изменённых файлов: 764 добавлений и 89 удалений

Двоичные данные
build/images/application-insights.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 7.7 KiB

Двоичные данные
build/images/bot-instrumented.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 19 KiB

27
client/@types/types.d.ts поставляемый
Просмотреть файл

@ -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') });
}