Merged editor settings
This commit is contained in:
Коммит
8eeca1bf76
|
@ -8,14 +8,14 @@
|
|||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Server",
|
||||
"program": "${workspaceRoot}\\server\\index.js",
|
||||
"program": "${workspaceRoot}/server/index.js",
|
||||
"outFiles": []
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"program": "${workspaceRoot}\\client:start",
|
||||
"program": "${workspaceRoot}/client:start",
|
||||
"outFiles": []
|
||||
},
|
||||
{
|
||||
|
|
|
@ -5,5 +5,8 @@
|
|||
},
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.detectIndentation": false
|
||||
"editor.detectIndentation": false,
|
||||
"editor.rulers": [
|
||||
120
|
||||
],
|
||||
}
|
47
docs/area.md
47
docs/area.md
|
@ -1,6 +1,6 @@
|
|||
# Area
|
||||
|
||||
This article explains how define a Area element which is composed of [AreaChart](http://recharts.org/#/en-US/api/AreaChart).
|
||||
This article explains how define an Area element. This element is composed of an [AreaChart](http://recharts.org/#/en-US/api/AreaChart) component.
|
||||
|
||||
## Basic properties
|
||||
|
||||
|
@ -22,7 +22,46 @@ Define `dependencies` as follows:
|
|||
| :--------|:-----|:-----------
|
||||
| `values`| `string` | Reference to data source values
|
||||
| `lines`| `string` | Reference to data source lines
|
||||
| `timeFormat`| `string` | Reference to data source lines
|
||||
| `timeFormat`| `string` | Reference to data source timeline
|
||||
|
||||
## AreaChart properties
|
||||
`areaProps` can be set to specify additional properties of the [AreaChart](http://recharts.org/#/en-US/api/AreaChart) chart component.
|
||||
#### Dependencies sample
|
||||
|
||||
```js
|
||||
dependencies: {
|
||||
values: "ai:timeline-graphData",
|
||||
lines: "ai:timeline-channels",
|
||||
timeFormat: "ai:timeline-timeFormat"
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
Define `props` as follows:
|
||||
|
||||
| Property | Type | Description
|
||||
| :--------|:-----|:-----------
|
||||
| `isStacked`| `boolean` | Display as stacked area
|
||||
| `showLegend`| `boolean` | Display legend
|
||||
| `areaProps`| `object` | [AreaChart](http://recharts.org/#/en-US/api/AreaChart) properties
|
||||
|
||||
#### Props sample
|
||||
|
||||
```js
|
||||
props: {
|
||||
isStacked: true,
|
||||
showLegend: false
|
||||
}
|
||||
```
|
||||
|
||||
#### AreaChart properties
|
||||
- Tip: `areaProps` can be used to specify additional properties of the [AreaChart](http://recharts.org/#/en-US/api/AreaChart) chart component such as `syncId` to link related charts. Refer to the [AreaChart API](http://recharts.org/#/en-US/api/AreaChart) for more info.
|
||||
|
||||
```js
|
||||
props: {
|
||||
isStacked: true,
|
||||
showLegend: false
|
||||
areaProps: {
|
||||
syncId: "sharedId"
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,79 @@
|
|||
# BarData
|
||||
|
||||
This article explains how define a BarData element. This element is composed of a [BarChart](http://recharts.org/#/en-US/api/BarChart) component.
|
||||
|
||||
## Basic properties
|
||||
|
||||
| Property | Type | Value | Description
|
||||
| :--------|:-----|:------|:------------
|
||||
| `id`| `string` || ID of the element on the page
|
||||
| `type`| `string` | "BarData" |
|
||||
| `title`| `string` || Title that will appear at the top of the view
|
||||
| `subtitle`| `string` || Description of the chart (displayed as tooltip)
|
||||
| `size`| `{ w: number, h: number}` || Width/Height of the view
|
||||
| `dependencies`| `object` || Dependencies that are required for this element
|
||||
| `props`| `object` || Additional properties to define for this element
|
||||
| `actions`| `object` || Actions to trigger when bar is clicked
|
||||
|
||||
## Dependencies
|
||||
|
||||
Define `dependencies` as follows:
|
||||
|
||||
| Property | Type | Description
|
||||
| :--------|:-----|:-----------
|
||||
| `values`| `string` | Reference to data source values
|
||||
| `bars`| `string` | Reference to data source bars
|
||||
|
||||
#### Dependencies sample
|
||||
|
||||
```js
|
||||
dependencies: {
|
||||
values: "ai:intents",
|
||||
bars: "ai:intents-bars",
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
Define `props` as follows:
|
||||
|
||||
| Property | Type | Description
|
||||
| :--------|:-----|:-----------
|
||||
| `nameKey`| `string` | Data key to use for x-axis
|
||||
| `barProps`| `object` | [BarChart](http://recharts.org/#/en-US/api/BarChart) properties
|
||||
|
||||
#### Props sample
|
||||
|
||||
```js
|
||||
props: {
|
||||
nameKey: "intent"
|
||||
}
|
||||
```
|
||||
|
||||
#### BarChart properties
|
||||
- Tip: `barProps` can be used to specify additional properties of the [BarChart](http://recharts.org/#/en-US/api/BarChart). Refer to the [BarChart API](http://recharts.org/#/en-US/api/BarChart) for more info.
|
||||
|
||||
## Actions
|
||||
|
||||
Define an `onBarClick` action as follows:
|
||||
|
||||
| Property | Type | Description
|
||||
| :--------|:-----|:-----------
|
||||
| `action`| `string` | Reference to dialog id
|
||||
| `params`| `object` | Arguments or properties that need to be passed to run the dialog's query
|
||||
|
||||
#### Actions sample
|
||||
|
||||
```js
|
||||
actions: {
|
||||
onBarClick: {
|
||||
action: "dialog:conversations",
|
||||
params: {
|
||||
title: "args:intent",
|
||||
intent: "args:intent",
|
||||
queryspan: "timespan:queryTimespan"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Dashboard Creation
|
||||
Creating a dashboard relies upon the following assumptions:
|
||||
|
||||
## Listing Available Templates
|
||||
The list of available template will appear in the home page and will enable the user to choose a template to create from.
|
||||
The following fields will appear in the list:
|
||||
|
||||
- `preview` (as an image)
|
||||
- `name`
|
||||
- `description`
|
||||
|
||||
# Creation Screen
|
||||
When clicking on one of the template a dialog will be open with the following fields:
|
||||
|
||||
- `id`: Will be used as an id for the dashboard and in the url
|
||||
- `name`: Will be used in the title of the dashboard
|
||||
- `icon`: Will be used in the navigation bar
|
|
@ -0,0 +1,53 @@
|
|||
# PieData
|
||||
|
||||
This article explains how define an PieData element. This element is composed of a [PieChart](http://recharts.org/#/en-US/api/PieChart) component.
|
||||
|
||||
## Basic properties
|
||||
|
||||
| Property | Type | Value | Description
|
||||
| :--------|:-----|:------|:------------
|
||||
| `id`| `string` || ID of the element on the page
|
||||
| `type`| `string` | "PieData" |
|
||||
| `title`| `string` || Title that will appear at the top of the view
|
||||
| `subtitle`| `string` || Description of the chart (displayed as tooltip)
|
||||
| `size`| `{ w: number, h: number}` || Width/Height of the view
|
||||
| `dependencies`| `object` || Dependencies that are required for this element
|
||||
| `props`| `object` || Additional properties to define for this element
|
||||
|
||||
## Dependencies
|
||||
|
||||
Define `dependencies` as follows:
|
||||
|
||||
| Property | Type | Description
|
||||
| :--------|:-----|:-----------
|
||||
| `values`| `string` | Reference to data source values
|
||||
|
||||
#### Dependencies sample
|
||||
|
||||
```js
|
||||
dependencies: {
|
||||
values: "ai:timeline-channelUsage",
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
Define `props` as follows:
|
||||
|
||||
| Property | Type | Description
|
||||
| :--------|:-----|:-----------
|
||||
| `showLegend`| `boolean` | Display legend
|
||||
| `compact`| `boolean` | Display as compact chart
|
||||
| `pieProps`| `object` | [PieChart](http://recharts.org/#/en-US/api/PieChart) properties
|
||||
|
||||
#### Props sample
|
||||
|
||||
```js
|
||||
props: {
|
||||
showLegend: false,
|
||||
compact: true
|
||||
}
|
||||
```
|
||||
|
||||
#### PieChart properties
|
||||
- Tip: `pieProps` can be used to specify additional properties of the [PieChart](http://recharts.org/#/en-US/api/PieChart). Refer to the [PieChart API](http://recharts.org/#/en-US/api/PieChart) for more info.
|
|
@ -0,0 +1,55 @@
|
|||
# Scatter
|
||||
|
||||
This article explains how define a Scatter element. This element is composed of an [ScatterChart](http://recharts.org/#/en-US/api/ScatterChart) component.
|
||||
|
||||
## Basic properties
|
||||
|
||||
| Property | Type | Value | Description
|
||||
| :--------|:-----|:------|:------------
|
||||
| `id`| `string` || ID of the element on the page
|
||||
| `type`| `string` | "Scatter" |
|
||||
| `title`| `string` || Title that will appear at the top of the view
|
||||
| `subtitle`| `string` || Description of the chart (displayed as tooltip)
|
||||
| `size`| `{ w: number, h: number}` || Width/Height of the view
|
||||
| `dependencies`| `object` || Dependencies that are required for this element
|
||||
| `props`| `object` || Additional properties to define for this element
|
||||
|
||||
## Dependencies
|
||||
|
||||
Define `dependencies` as follows:
|
||||
|
||||
| Property | Type | Description
|
||||
| :--------|:-----|:-----------
|
||||
| `groupedValues`| `string` | Reference to data source grouped values. Each group is a Scatter line.
|
||||
|
||||
#### Dependencies sample
|
||||
|
||||
```js
|
||||
dependencies: {
|
||||
groupedValues: "ai:channelActivity-groupedValues"
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
Define `props` as follows:
|
||||
|
||||
| Property | Type | Description
|
||||
| :--------|:-----|:-----------
|
||||
| `xDataKey`| `string` | x-axis data key
|
||||
| `yDataKey`| `string` | y-axis data key
|
||||
| `zDataKey`| `string` | z-axis data key
|
||||
| `zRange`| `number[]` | Range of the z-axis used for scale of scatter points.
|
||||
| `scatterProps`| `object` | [ScatterChart](http://recharts.org/#/en-US/api/ScatterChart) properties
|
||||
|
||||
```js
|
||||
props: {
|
||||
xDataKey: "hourOfDay",
|
||||
yDataKey: "duration",
|
||||
zDataKey: "count",
|
||||
zRange: [10,500]
|
||||
}
|
||||
```
|
||||
|
||||
#### ScatterChart properties
|
||||
- Tip: `scatterProps` can be used to specify additional properties of the [ScatterChart](http://recharts.org/#/en-US/api/ScatterChart). Refer to the [ScatterChart API](http://recharts.org/#/en-US/api/ScatterChart) for more info.
|
|
@ -19,7 +19,6 @@ There are 3 configurations for this element type
|
|||
|
||||
## Single value
|
||||
|
||||
To read how to define dependencies [click here](/dependencies).
|
||||
Define `dependencies` as follows:
|
||||
|
||||
| Property | Description
|
||||
|
@ -35,7 +34,7 @@ Define `properties` as follow:
|
|||
| Property | Type | Description
|
||||
| :--------|:-----|:------------
|
||||
| `subheading`| `string` | Large value to display
|
||||
| `onClick`| `string` | Action name [[Read about actions](/actions)]
|
||||
| `onClick`| `string` | Action name
|
||||
|
||||
```js
|
||||
{
|
||||
|
@ -56,7 +55,6 @@ Define `properties` as follow:
|
|||
|
||||
## Multiple values
|
||||
|
||||
To read how to define dependencies [click here](/dependencies).
|
||||
Define `dependencies` as follows:
|
||||
|
||||
| Property | Description
|
||||
|
|
|
@ -23,8 +23,6 @@ Define `dependencies` as follows:
|
|||
| `groups`| `string` | Reference to collection of grouped values
|
||||
| `values`| `string` | Reference to values loaded from a selected group
|
||||
|
||||
To read how to define dependencies [click here](/dependencies).
|
||||
|
||||
#### Dependencies sample:
|
||||
|
||||
```js
|
||||
|
|
|
@ -22,8 +22,6 @@ Define `dependencies` as follows:
|
|||
| :--------|:-----|:-----------
|
||||
| `values`| `string` | Reference to values loaded from data source
|
||||
|
||||
To read how to define dependencies [click here](/dependencies).
|
||||
|
||||
#### Dependencies sample:
|
||||
|
||||
```js
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
# Timeline
|
||||
|
||||
This article explains how define a Timeline element. This element is composed of an [LineChart](http://recharts.org/#/en-US/api/LineChart) component.
|
||||
|
||||
## Basic properties
|
||||
|
||||
| Property | Type | Value | Description
|
||||
| :--------|:-----|:------|:------------
|
||||
| `id`| `string` || ID of the element on the page
|
||||
| `type`| `string` | "Timeline" |
|
||||
| `title`| `string` || Title that will appear at the top of the view
|
||||
| `subtitle`| `string` || Description of the chart (displayed as tooltip)
|
||||
| `size`| `{ w: number, h: number}` || Width/Height of the view
|
||||
| `dependencies`| `object` || Dependencies that are required for this element
|
||||
| `props`| `object` || Additional properties to define for this element
|
||||
|
||||
## Dependencies
|
||||
|
||||
Define `dependencies` as follows:
|
||||
|
||||
| Property | Type | Description
|
||||
| :--------|:-----|:-----------
|
||||
| `values`| `string` | Reference to data source values
|
||||
| `lines`| `string` | Reference to data source lines
|
||||
| `timeFormat`| `string` | Reference to data source timeline
|
||||
|
||||
#### Dependencies sample
|
||||
|
||||
```js
|
||||
dependencies: {
|
||||
values: "ai:timeline-graphData",
|
||||
lines: "ai:timeline-channels",
|
||||
timeFormat: "ai:timeline-timeFormat"
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
Define `props` as follows:
|
||||
|
||||
| Property | Type | Description
|
||||
| :--------|:-----|:-----------
|
||||
| `lineProps`| `object` | [LineChart](http://recharts.org/#/en-US/api/LineChart) properties
|
||||
|
||||
#### LineChart properties
|
||||
- Tip: `lineProps` can be used to specify additional properties of the [LineChart](http://recharts.org/#/en-US/api/LineChart) chart component such as `syncId` to link related charts. Refer to the [LineChart API](http://recharts.org/#/en-US/api/LineChart) for more info.
|
||||
|
||||
```js
|
||||
props: {
|
||||
lineProps: {
|
||||
syncId: "sharedId"
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,72 @@
|
|||
# Two Modes Element
|
||||
This article describes how to create two modes for an element in the dashboard
|
||||
|
||||
## Data Source
|
||||
First you'll need to create a data source that exposes a set boolean variable:
|
||||
|
||||
```js
|
||||
{
|
||||
id: "modes",
|
||||
type: "Constant",
|
||||
params: {
|
||||
values: ["messages","users"],
|
||||
selectedValue: "messages"
|
||||
},
|
||||
calculated: (state, dependencies) => {
|
||||
let flags = {};
|
||||
flags['messages'] = (state.selectedValue === 'messages');
|
||||
flags['users'] = (state.selectedValue !== 'messages');
|
||||
return flags;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Second, you'll need to create a filter to control that constant:
|
||||
|
||||
```js
|
||||
{
|
||||
type: "TextFilter",
|
||||
dependencies: {
|
||||
selectedValue: "modes",
|
||||
values: "modes:values"
|
||||
},
|
||||
actions: {
|
||||
onChange: "modes:updateSelectedValue"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And last, you need to create two instances of the same element (with the same id), where each appears only when their flag is on:
|
||||
|
||||
```js
|
||||
elements: [
|
||||
{
|
||||
id: "timeline",
|
||||
type: "Timeline",
|
||||
title: "Message Rate",
|
||||
subtitle: "How many messages were sent per timeframe",
|
||||
size: { w: 5, h: 8 },
|
||||
dependencies: {
|
||||
visible: "modes:messages",
|
||||
values: "ai:timeline-graphData",
|
||||
lines: "ai:timeline-channels",
|
||||
timeFormat: "ai:timeline-timeFormat"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "timeline",
|
||||
type: "Timeline",
|
||||
title: "Users Rate",
|
||||
subtitle: "How many users were sent per timeframe",
|
||||
size: { w: 5, h: 8 },
|
||||
dependencies: {
|
||||
visible: "modes:users",
|
||||
values: "ai:timeline-users-graphData",
|
||||
lines: "ai:timeline-users-channels",
|
||||
timeFormat: "ai:timeline-users-timeFormat"
|
||||
}
|
||||
}
|
||||
}]
|
||||
```
|
||||
|
||||
Notice that each instance has a `visible` property defined under dependencies and a different set of properties and `dependencies`.
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 117 KiB |
|
@ -1,4 +1,8 @@
|
|||
return {
|
||||
id: 'bot_health_dashboard',
|
||||
name: 'Bot Health Dashboard',
|
||||
description: 'Microsoft Bot Framework based health',
|
||||
preview: '/images/bot-framework-preview.png',
|
||||
config: {
|
||||
connections: { },
|
||||
layout: {
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,4 +1,10 @@
|
|||
return {
|
||||
id: 'bot_analytics_dashboard',
|
||||
name: 'Bot Analytics Dashboard',
|
||||
icon: "dashboard",
|
||||
url: "bot_analytics_dashboard",
|
||||
description: 'Microsoft Bot Framework based analytics',
|
||||
preview: '/images/bot-framework-preview.png',
|
||||
config: {
|
||||
connections: { },
|
||||
layout: {
|
||||
|
@ -385,6 +391,20 @@ return {
|
|||
return { queryTimespan, granularity };
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "modes",
|
||||
type: "Constant",
|
||||
params: {
|
||||
values: ["messages","users"],
|
||||
selectedValue: "messages"
|
||||
},
|
||||
calculated: (state, dependencies) => {
|
||||
let flags = {};
|
||||
flags['messages'] = (state.selectedValue === 'messages');
|
||||
flags['users'] = (state.selectedValue !== 'messages');
|
||||
return flags;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "filters",
|
||||
type: "ApplicationInsights/Query",
|
||||
|
@ -406,12 +426,17 @@ return {
|
|||
channel: (val) => val || "unknown",
|
||||
channel_count: (val) => val || 0
|
||||
},
|
||||
calculated: (filterChannels) => {
|
||||
calculated: (filterChannels, dependencies, prevState) => {
|
||||
|
||||
// This code is meant to fix the following scenario:
|
||||
// When "Timespan" filter changes, to "channels-selected" variable
|
||||
// is going to be reset into an empty set.
|
||||
// For this reason, using previous state to copy filter
|
||||
const filters = filterChannels.map((x) => x.channel);
|
||||
let { selectedValues } = filterChannels;
|
||||
if (selectedValues === undefined) {
|
||||
selectedValues = [];
|
||||
}
|
||||
let selectedValues = [];
|
||||
if (prevState['channels-selected'] !== undefined) {
|
||||
selectedValues = prevState['channels-selected'];
|
||||
}
|
||||
return {
|
||||
"channels-count": filterChannels,
|
||||
"channels-filters": filters,
|
||||
|
@ -429,12 +454,12 @@ return {
|
|||
intent: (val) => val || "unknown",
|
||||
intent_count: (val) => val || 0
|
||||
},
|
||||
calculated: (filterIntents) => {
|
||||
calculated: (filterIntents, dependencies, prevState) => {
|
||||
const intents = filterIntents.map((x) => x.intent);
|
||||
let { selectedValues } = filterIntents;
|
||||
if (selectedValues === undefined) {
|
||||
selectedValues = [];
|
||||
}
|
||||
let selectedValues = [];
|
||||
if (prevState['intents-selected'] !== undefined) {
|
||||
selectedValues = prevState['intents-selected'];
|
||||
}
|
||||
return {
|
||||
"intents-count": filterIntents,
|
||||
"intents-filters": intents,
|
||||
|
@ -554,6 +579,64 @@ return {
|
|||
};
|
||||
}
|
||||
},
|
||||
users_timeline: {
|
||||
query: (dependencies) => {
|
||||
var { granularity } = dependencies;
|
||||
return `` +
|
||||
` where name == 'Activity' |` +
|
||||
` summarize count=dcount(tostring(customDimensions.from)) by bin(timestamp, ${granularity}), name, channel=tostring(customDimensions.channel) |` +
|
||||
` order by timestamp asc`
|
||||
},
|
||||
mappings: {
|
||||
channel: (val) => val || "unknown",
|
||||
count: (val) => val || 0
|
||||
},
|
||||
filters: [{
|
||||
dependency: "selectedChannels",
|
||||
queryProperty: "customDimensions.channel"
|
||||
}],
|
||||
calculated: (timeline, dependencies) => {
|
||||
|
||||
// Timeline handling
|
||||
// =================
|
||||
|
||||
let _timeline = {};
|
||||
let _channels = {};
|
||||
let { timespan } = dependencies;
|
||||
|
||||
timeline.forEach(row => {
|
||||
var { channel, timestamp, count } = row;
|
||||
var timeValue = (new Date(timestamp)).getTime();
|
||||
|
||||
if (!_timeline[timeValue]) _timeline[timeValue] = {
|
||||
time: (new Date(timestamp)).toUTCString()
|
||||
};
|
||||
if (!_channels[channel]) _channels[channel] = {
|
||||
name: channel,
|
||||
value: 0
|
||||
};
|
||||
|
||||
_timeline[timeValue][channel] = count;
|
||||
_channels[channel].value += count;
|
||||
});
|
||||
|
||||
var channels = Object.keys(_channels);
|
||||
var channelUsage = _.values(_channels);
|
||||
var timelineValues = _.map(_timeline, value => {
|
||||
channels.forEach(channel => {
|
||||
if (!value[channel]) value[channel] = 0;
|
||||
});
|
||||
return value;
|
||||
});
|
||||
|
||||
return {
|
||||
"timeline-users-graphData": timelineValues,
|
||||
"timeline-users-channelUsage": channelUsage,
|
||||
"timeline-users-timeFormat": (timespan === "24 hours" ? 'hour' : 'date'),
|
||||
"timeline-users-channels": channels
|
||||
};
|
||||
}
|
||||
},
|
||||
intents: {
|
||||
query: () => `` +
|
||||
` extend cslen = customDimensions.callstack_length, intent=customDimensions.intent | ` +
|
||||
|
@ -688,10 +771,18 @@ return {
|
|||
filters: [
|
||||
{
|
||||
type: "TextFilter",
|
||||
title: "Timespan",
|
||||
dependencies: { selectedValue: "timespan", values: "timespan:values" },
|
||||
actions: { onChange: "timespan:updateSelectedValue" },
|
||||
first: true
|
||||
},
|
||||
{
|
||||
type: "TextFilter",
|
||||
title: "Mode",
|
||||
dependencies: { selectedValue: "modes", values: "modes:values" },
|
||||
actions: { onChange: "modes:updateSelectedValue" },
|
||||
first: true
|
||||
},
|
||||
{
|
||||
type: "MenuFilter",
|
||||
title: "Channels",
|
||||
|
@ -728,19 +819,34 @@ return {
|
|||
title: "Message Rate",
|
||||
subtitle: "How many messages were sent per timeframe",
|
||||
size: { w: 5, h: 8 },
|
||||
dependencies: { values: "ai:timeline-graphData", lines: "ai:timeline-channels", timeFormat: "ai:timeline-timeFormat" }
|
||||
},
|
||||
dependencies: { visible: "modes:messages", values: "ai:timeline-graphData", lines: "ai:timeline-channels", timeFormat: "ai:timeline-timeFormat" }
|
||||
},
|
||||
{
|
||||
id: "timeline",
|
||||
type: "Timeline",
|
||||
title: "Users Rate",
|
||||
subtitle: "How many users were sent per timeframe",
|
||||
size: { w: 5, h: 8 },
|
||||
dependencies: { visible: "modes:users", values: "ai:timeline-users-graphData", lines: "ai:timeline-users-channels", timeFormat: "ai:timeline-users-timeFormat" }
|
||||
},
|
||||
{
|
||||
id: "channels",
|
||||
type: "PieData",
|
||||
title: "Channel Usage",
|
||||
subtitle: "Total messages sent per channel",
|
||||
size: { w: 3, h: 8 },
|
||||
dependencies: { values: "ai:timeline-channelUsage" },
|
||||
props: {
|
||||
showLegend: false
|
||||
}
|
||||
},
|
||||
dependencies: { visible: "modes:messages", values: "ai:timeline-channelUsage" },
|
||||
props: { showLegend: false, compact: true }
|
||||
},
|
||||
{
|
||||
id: "channels",
|
||||
type: "PieData",
|
||||
title: "Channel Usage (Users)",
|
||||
subtitle: "Total users sent per channel",
|
||||
size: { w: 3, h: 8 },
|
||||
dependencies: { visible: "modes:users", values: "ai:timeline-users-channelUsage" },
|
||||
props: { showLegend: false, compact: true }
|
||||
},
|
||||
{
|
||||
id: "scores",
|
||||
type: "Scorecard",
|
||||
|
|
|
@ -6,45 +6,209 @@ const privateSetupPath = path.join(__dirname, '..', 'config', 'setup.private.jso
|
|||
const initialSetupPath = path.join(__dirname, '..', 'config', 'setup.initial.json');
|
||||
const router = new express.Router();
|
||||
|
||||
router.get('/dashboard.js', (req, res) => {
|
||||
router.get('/dashboards', (req, res) => {
|
||||
|
||||
let privateDashboard = path.join(__dirname, '..', 'dashboards', 'dashboard.private.js');
|
||||
let preconfDashboard = path.join(__dirname, '..', 'dashboards', 'preconfigured', 'bot-framework.js');
|
||||
let dashboardPath = fs.existsSync(privateDashboard) ? privateDashboard : preconfDashboard;
|
||||
let privateDashboard = path.join(__dirname, '..', 'dashboards');
|
||||
let preconfDashboard = path.join(__dirname, '..', 'dashboards', 'preconfigured');
|
||||
|
||||
fs.readFile(dashboardPath, 'utf8', (err, data) => {
|
||||
if (err) throw err;
|
||||
let script = '';
|
||||
let files = fs.readdirSync(privateDashboard);
|
||||
if (files && files.length) {
|
||||
files.forEach((fileName) => {
|
||||
let filePath = path.join(privateDashboard, fileName);
|
||||
let stats = fs.statSync(filePath);
|
||||
if (stats.isFile() && filePath.endsWith('.js')) {
|
||||
let json = getJSONFromScript(filePath);
|
||||
let jsonDefinition = {
|
||||
id: json.id,
|
||||
name: json.name,
|
||||
description: json.description,
|
||||
icon: json.icon,
|
||||
url: json.url,
|
||||
preview: json.preview
|
||||
};
|
||||
let content = 'return ' + JSON.stringify(jsonDefinition);
|
||||
|
||||
// Ensuing this dashboard is loaded into the dashboards array on the page
|
||||
let script = `
|
||||
(function (window) {
|
||||
var dashboard = (function () {
|
||||
${data}
|
||||
})();
|
||||
window.dashboards = window.dashboards || [];
|
||||
window.dashboards.push(dashboard);
|
||||
})(window);
|
||||
`;
|
||||
// Ensuing this dashboard is loaded into the dashboards array on the page
|
||||
script += `
|
||||
(function (window) {
|
||||
var dashboard = (function () {
|
||||
${content}
|
||||
})();
|
||||
window.dashboardDefinitions = window.dashboardDefinitions || [];
|
||||
window.dashboardDefinitions.push(dashboard);
|
||||
})(window);
|
||||
`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.send(script);
|
||||
});
|
||||
let templates = fs.readdirSync(preconfDashboard);
|
||||
if (templates && templates.length) {
|
||||
templates.forEach((fileName) => {
|
||||
let filePath = path.join(preconfDashboard, fileName);
|
||||
let stats = fs.statSync(filePath);
|
||||
if (stats.isFile() && filePath.endsWith('.js')) {
|
||||
let json = getJSONFromScript(filePath);
|
||||
let jsonDefinition = {
|
||||
id: json.id,
|
||||
name: json.name,
|
||||
description: json.description,
|
||||
icon: json.icon,
|
||||
url: json.url,
|
||||
preview: json.preview
|
||||
};
|
||||
let content = 'return ' + JSON.stringify(jsonDefinition);
|
||||
|
||||
// Ensuing this dashboard is loaded into the dashboards array on the page
|
||||
script += `
|
||||
(function (window) {
|
||||
var dashboardTemplate = (function () {
|
||||
${content}
|
||||
})();
|
||||
window.dashboardTemplates = window.dashboardTemplates || [];
|
||||
window.dashboardTemplates.push(dashboardTemplate);
|
||||
})(window);
|
||||
`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.send(script);
|
||||
});
|
||||
|
||||
router.post('/dashboard.js', (req, res) => {
|
||||
var content = (req.body && req.body.script) || '';
|
||||
console.dir(content);
|
||||
router.get('/dashboards/:id', (req, res) => {
|
||||
|
||||
fs.writeFile(path.join(__dirname, '..', 'dashboards', 'dashboard.private.js'), content, err => {
|
||||
let dashboardId = req.params.id;
|
||||
let privateDashboard = path.join(__dirname, '..', 'dashboards');
|
||||
|
||||
let script = '';
|
||||
let dashboardFile = getFileById(privateDashboard, dashboardId);
|
||||
|
||||
if (dashboardFile) {
|
||||
let filePath = path.join(privateDashboard, dashboardFile);
|
||||
let stats = fs.statSync(filePath);
|
||||
if (stats.isFile() && filePath.endsWith('.js')) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Ensuing this dashboard is loaded into the dashboards array on the page
|
||||
script += `
|
||||
(function (window) {
|
||||
var dashboard = (function () {
|
||||
${content}
|
||||
})();
|
||||
window.dashboard = dashboard || null;
|
||||
})(window);
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
res.send(script);
|
||||
});
|
||||
|
||||
router.post('/dashboards/:id', (req, res) => {
|
||||
let { id } = req.params;
|
||||
let { script } = req.body || '';
|
||||
|
||||
let privateDashboard = path.join(__dirname, '..', 'dashboards');
|
||||
let dashboardFile = getFileById(privateDashboard, id);
|
||||
let filePath = path.join(privateDashboard, dashboardFile);
|
||||
|
||||
fs.writeFile(filePath, script, err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.end(err);
|
||||
}
|
||||
|
||||
res.end(content);
|
||||
res.json({ script });
|
||||
})
|
||||
});
|
||||
|
||||
function isAuthoeizedToSetup(req) {
|
||||
router.get('/templates/:id', (req, res) => {
|
||||
|
||||
let templateId = req.params.id;
|
||||
let preconfDashboard = path.join(__dirname, '..', 'dashboards', 'preconfigured');
|
||||
|
||||
let script = '';
|
||||
let dashboardFile = getFileById(preconfDashboard, templateId);
|
||||
|
||||
if (dashboardFile) {
|
||||
let filePath = path.join(preconfDashboard, dashboardFile);
|
||||
let stats = fs.statSync(filePath);
|
||||
if (stats.isFile() && filePath.endsWith('.js')) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Ensuing this dashboard is loaded into the dashboards array on the page
|
||||
script += `
|
||||
(function (window) {
|
||||
var template = (function () {
|
||||
${content}
|
||||
})();
|
||||
window.template = template || null;
|
||||
})(window);
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
res.send(script);
|
||||
});
|
||||
|
||||
router.put('/dashboards/:id', (req, res) => {
|
||||
let { id } = req.params;
|
||||
let { script } = req.body || '';
|
||||
|
||||
let privateDashboard = path.join(__dirname, '..', 'dashboards');
|
||||
let dashboardPath = path.join(privateDashboard, id + '.private.js');
|
||||
let dashboardFile = getFileById(privateDashboard, id);
|
||||
let dashboardExists = fs.existsSync(dashboardPath);
|
||||
|
||||
if (dashboardFile || dashboardExists) {
|
||||
return res.json({ errors: ['Dashboard id or filename already exists'] });
|
||||
}
|
||||
|
||||
fs.writeFile(dashboardPath, script, err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.end(err);
|
||||
}
|
||||
|
||||
res.json({ script });
|
||||
});
|
||||
});
|
||||
|
||||
function getFileById(dir, id) {
|
||||
let files = fs.readdirSync(dir) || [];
|
||||
|
||||
// Make sure the array only contains files
|
||||
files = files.filter(fileName => fs.statSync(path.join(dir, fileName)).isFile());
|
||||
|
||||
let dashboardFile = null;
|
||||
if (files.length) {
|
||||
|
||||
let dashboardIndex = parseInt(id);
|
||||
if (!isNaN(dashboardIndex) && files.length > dashboardIndex) {
|
||||
dashboardFile = files[dashboardIndex];
|
||||
}
|
||||
|
||||
if (!dashboardFile) {
|
||||
files.forEach(fileName => {
|
||||
let filePath = path.join(dir, fileName);
|
||||
|
||||
let stats = fs.statSync(filePath);
|
||||
if (stats.isFile() && filePath.endsWith('.js')) {
|
||||
let dashboard = getJSONFromScript(filePath);
|
||||
if (dashboard.id && dashboard.id === id) {
|
||||
dashboardFile = fileName;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return dashboardFile;
|
||||
}
|
||||
|
||||
function isAuthorizedToSetup(req) {
|
||||
if (!fs.existsSync(privateSetupPath)) { return true; }
|
||||
|
||||
let configString = fs.readFileSync(privateSetupPath, 'utf8');
|
||||
|
@ -61,7 +225,7 @@ function isAuthoeizedToSetup(req) {
|
|||
|
||||
router.get('/setup', (req, res) => {
|
||||
|
||||
if (!isAuthoeizedToSetup(req)) {
|
||||
if (!isAuthorizedToSetup(req)) {
|
||||
return res.send({ error: new Error('User is not authorized to setup') });
|
||||
}
|
||||
|
||||
|
@ -76,7 +240,7 @@ router.get('/setup', (req, res) => {
|
|||
|
||||
router.post('/setup', (req, res) => {
|
||||
|
||||
if (!isAuthoeizedToSetup(req)) {
|
||||
if (!isAuthorizedToSetup(req)) {
|
||||
return res.send({ error: new Error('User is not authorized to setup') });
|
||||
}
|
||||
|
||||
|
@ -95,4 +259,21 @@ router.post('/setup', (req, res) => {
|
|||
|
||||
module.exports = {
|
||||
router
|
||||
}
|
||||
|
||||
function getJSONFromScript(filePath) {
|
||||
if (!fs.existsSync(filePath)) { return {}; }
|
||||
|
||||
let jsonScript = {};
|
||||
let stats = fs.statSync(filePath);
|
||||
if (stats.isFile() && filePath.endsWith('.js')) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
try {
|
||||
eval('jsonScript = (function () { ' + content + ' })();');
|
||||
} catch (e) {
|
||||
console.warn('Parse error with template:', filePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
return jsonScript;
|
||||
}
|
|
@ -3,6 +3,9 @@ import * as request from 'xhr-request';
|
|||
|
||||
interface IConfigurationsActions {
|
||||
loadConfiguration(): any;
|
||||
loadDashboard(id: string): any;
|
||||
createDashboard(dashboard: IDashboardConfig): any;
|
||||
loadTemplate(id: string): any;
|
||||
saveConfiguration(dashboard: IDashboardConfig): any;
|
||||
failure(error: any): void;
|
||||
}
|
||||
|
@ -14,17 +17,70 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
|
|||
|
||||
loadConfiguration() {
|
||||
|
||||
return (dispatcher: (dashboard: IDashboardConfig) => void) => {
|
||||
return (dispatcher: (result: { dashboards: IDashboardConfig[], templates: IDashboardConfig[] }) => void) => {
|
||||
|
||||
this.getScript('/api/dashboard.js', () => {
|
||||
let dashboards: IDashboardConfig[] = (window as any)['dashboards'];
|
||||
this.getScript('/api/dashboards', () => {
|
||||
let dashboards: IDashboardConfig[] = (window as any)['dashboardDefinitions'];
|
||||
let templates: IDashboardConfig[] = (window as any)['dashboardTemplates'];
|
||||
|
||||
if (!dashboards || !dashboards.length) {
|
||||
return this.failure(new Error('Could not load configuration'));
|
||||
// if (!dashboards || !dashboards.length) {
|
||||
// return this.failure(new Error('Could not load configuration'));
|
||||
// }
|
||||
|
||||
return dispatcher({ dashboards, templates });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
loadDashboard(id: string) {
|
||||
|
||||
return (dispatcher: (result: { dashboard: IDashboardConfig }) => void) => {
|
||||
|
||||
this.getScript('/api/dashboards/' + id, () => {
|
||||
let dashboard: IDashboardConfig = (window as any)['dashboard'];
|
||||
|
||||
if (!dashboard) {
|
||||
return this.failure(new Error('Could not load configuration for dashboard ' + id));
|
||||
}
|
||||
|
||||
let dashboard = dashboards[0];
|
||||
return dispatcher(dashboard);
|
||||
return dispatcher({ dashboard });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
createDashboard(dashboard: IDashboardConfig) {
|
||||
return (dispatcher: (dashboard: IDashboardConfig) => void) => {
|
||||
|
||||
let script = this.objectToString(dashboard);
|
||||
request('/api/dashboards/' + dashboard.id, {
|
||||
method: 'PUT',
|
||||
json: true,
|
||||
body: { script: 'return ' + script }
|
||||
},
|
||||
(error: any, json: any) => {
|
||||
|
||||
if (error || (json && json.errors)) {
|
||||
return this.failure(error || json.errors);
|
||||
}
|
||||
|
||||
return dispatcher(json);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
loadTemplate(id: string) {
|
||||
|
||||
return (dispatcher: (result: { template: IDashboardConfig }) => void) => {
|
||||
|
||||
this.getScript('/api/templates/' + id, () => {
|
||||
let template: IDashboardConfig = (window as any)['template'];
|
||||
|
||||
if (!template) {
|
||||
return this.failure(new Error('Could not load configuration for template ' + id));
|
||||
}
|
||||
|
||||
return dispatcher({ template });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -34,7 +90,7 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
|
|||
|
||||
let stringDashboard = this.objectToString(dashboard);
|
||||
|
||||
request('/api/dashboard.js', {
|
||||
request('/api/dashboards/' + dashboard.id, {
|
||||
method: 'POST',
|
||||
json: true,
|
||||
body: { script: 'return ' + stringDashboard }
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import alt, { AbstractActions } from '../alt';
|
||||
|
||||
interface IVisibilityActions {
|
||||
setFlags(flags: IDict<boolean>): any;
|
||||
turnFlagOn(flagName: string): any;
|
||||
turnFlagOff(flagName: string): any;
|
||||
}
|
||||
|
||||
class VisibilityActions extends AbstractActions implements IVisibilityActions {
|
||||
constructor(alt: AltJS.Alt) {
|
||||
super(alt);
|
||||
}
|
||||
|
||||
setFlags(flags: IDict<boolean>): any {
|
||||
return flags;
|
||||
}
|
||||
|
||||
initializeFlag(flagName: string): any {
|
||||
|
||||
}
|
||||
|
||||
turnFlagOn(flagName: string): any {
|
||||
let flag = {};
|
||||
flag[flagName] = true;
|
||||
return flag;
|
||||
}
|
||||
turnFlagOff(flagName: string): any {
|
||||
let flag = {};
|
||||
flag[flagName] = false;
|
||||
return flag;
|
||||
}
|
||||
}
|
||||
|
||||
const visibilityActions = alt.createActions<IVisibilityActions>(VisibilityActions);
|
||||
|
||||
export default visibilityActions;
|
|
@ -34,11 +34,9 @@ export default class ConfigDashboard extends React.Component<IConfigDashboardPro
|
|||
|
||||
this.onSave = this.onSave.bind(this);
|
||||
this.onSaveGoToDashboard = this.onSaveGoToDashboard.bind(this);
|
||||
|
||||
ConfigurationsActions.loadConfiguration();
|
||||
}
|
||||
|
||||
onParamChange(connectionKey, paramKey, value) {
|
||||
onParamChange(connectionKey: string, paramKey: string, value: any) {
|
||||
let { connections } = this.state;
|
||||
connections[connectionKey] = connections[connectionKey] || {};
|
||||
connections[connectionKey][paramKey] = value;
|
||||
|
@ -69,13 +67,16 @@ export default class ConfigDashboard extends React.Component<IConfigDashboardPro
|
|||
onSaveGoToDashboard() {
|
||||
this.onSave();
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.replace('/dashboard');
|
||||
}, 2000);
|
||||
setTimeout(
|
||||
() => {
|
||||
window.location.reload();
|
||||
},
|
||||
2000
|
||||
);
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
window.location.replace('/dashboard');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -16,6 +16,7 @@ import { loadDialogsFromDashboard } from '../generic/Dialogs';
|
|||
|
||||
import ConfigurationsActions from '../../actions/ConfigurationsActions';
|
||||
import ConfigurationsStore from '../../stores/ConfigurationsStore';
|
||||
import VisibilityStore from '../../stores/VisibilityStore';
|
||||
|
||||
interface IDashboardState {
|
||||
editMode?: boolean,
|
||||
|
@ -24,6 +25,7 @@ interface IDashboardState {
|
|||
currentBreakpoint?: string;
|
||||
layouts?: ILayouts;
|
||||
grid?: any;
|
||||
visibilityFlags?: IDict<boolean>;
|
||||
}
|
||||
|
||||
interface IDashboardProps {
|
||||
|
@ -40,7 +42,8 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
currentBreakpoint: 'lg',
|
||||
mounted: false,
|
||||
layouts: { },
|
||||
grid: null
|
||||
grid: null,
|
||||
visibilityFlags: {}
|
||||
};
|
||||
|
||||
constructor(props: IDashboardProps) {
|
||||
|
@ -53,6 +56,10 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
this.onDeleteDashboard = this.onDeleteDashboard.bind(this);
|
||||
this.onDeleteDashboardApprove = this.onDeleteDashboardApprove.bind(this);
|
||||
this.onDeleteDashboardCancel = this.onDeleteDashboardCancel.bind(this);
|
||||
|
||||
VisibilityStore.listen(state => {
|
||||
this.setState({ visibilityFlags: state.flags });
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -114,7 +121,10 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
let { dashboard } = this.props;
|
||||
dashboard.config.layout.layouts = dashboard.config.layout.layouts || {};
|
||||
dashboard.config.layout.layouts[breakpoint] = layout;
|
||||
ConfigurationsActions.saveConfiguration(dashboard);
|
||||
|
||||
if (this.state.editMode) {
|
||||
ConfigurationsActions.saveConfiguration(dashboard);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
}
|
||||
|
|
|
@ -2,8 +2,12 @@ import * as React from 'react';
|
|||
import * as _ from 'lodash';
|
||||
import plugins from './generic/plugins';
|
||||
|
||||
import { DataSourceConnector } from '../data-sources/DataSourceConnector';
|
||||
import VisibilityActions from '../actions/VisibilityActions';
|
||||
import VisibilityStore from '../stores/VisibilityStore';
|
||||
|
||||
export default class ElementConnector {
|
||||
static loadLayoutFromDashboard(elementsContainer: IElementsContainer, dashboard: IDashboardConfig) : ILayouts {
|
||||
static loadLayoutFromDashboard(elementsContainer: IElementsContainer, dashboard: IDashboardConfig): ILayouts {
|
||||
|
||||
var layouts = {};
|
||||
_.each(dashboard.config.layout.cols, (totalColumns, key) => {
|
||||
|
@ -23,11 +27,11 @@ export default class ElementConnector {
|
|||
|
||||
layouts[key] = layouts[key] || [];
|
||||
layouts[key].push({
|
||||
"i": id,
|
||||
"x": curCol,
|
||||
"y": curRowOffset,
|
||||
"w": size.w,
|
||||
"h": size.h
|
||||
'i': id,
|
||||
'x': curCol,
|
||||
'y': curRowOffset,
|
||||
'w': size.w,
|
||||
'h': size.h
|
||||
});
|
||||
|
||||
curCol += size.w;
|
||||
|
@ -40,33 +44,50 @@ export default class ElementConnector {
|
|||
|
||||
static loadElementsFromDashboard(dashboard: IElementsContainer, layout: ILayout[]): React.Component<any, any>[] {
|
||||
var elements = [];
|
||||
var elementId = {};
|
||||
var visibilityFlags = (VisibilityStore.getState() || {}).flags || {};
|
||||
|
||||
dashboard.elements.forEach((element, idx) => {
|
||||
var ReactElement = plugins[element.type];
|
||||
var { id, dependencies, actions, props, title, subtitle, size, theme } = element;
|
||||
var layoutProps = _.find(layout, { "i": id });
|
||||
var layoutProps = _.find(layout, { 'i': id });
|
||||
|
||||
if (dependencies && dependencies.visible && !visibilityFlags[dependencies.visible]) {
|
||||
if (typeof visibilityFlags[dependencies.visible] === 'undefined') {
|
||||
let flagDependencies = DataSourceConnector.extrapolateDependencies({ value: dependencies.visible });
|
||||
let flag = {};
|
||||
flag[dependencies.visible] = flagDependencies.dataSources.value || true;
|
||||
|
||||
(VisibilityActions.setFlags as any).defer(flag);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (elementId[id]) { return; }
|
||||
|
||||
elementId[id] = true;
|
||||
elements.push(
|
||||
<div key={id}>
|
||||
<ReactElement
|
||||
key={idx}
|
||||
dependencies={dependencies}
|
||||
actions={actions || {}}
|
||||
props={props || {}}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
layout={layoutProps}
|
||||
theme={theme}
|
||||
id={id + idx}
|
||||
dependencies={dependencies}
|
||||
actions={actions || {}}
|
||||
props={props || {}}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
layout={layoutProps}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
static loadFiltersFromDashboard(dashboard: IDashboardConfig): {
|
||||
filters : React.Component<any, any>[],
|
||||
filters: React.Component<any, any>[],
|
||||
additionalFilters: React.Component<any, any>[]
|
||||
} {
|
||||
var filters = [];
|
||||
|
@ -82,7 +103,7 @@ export default class ElementConnector {
|
|||
subtitle={element.subtitle}
|
||||
icon={element.icon}
|
||||
/>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return { filters, additionalFilters };
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import * as React from 'react';
|
||||
import Button from 'react-md/lib/Buttons/Button';
|
||||
import CircularProgress from 'react-md/lib/Progress/CircularProgress';
|
||||
import { Card, CardTitle, CardActions, CardText } from 'react-md/lib/Cards';
|
||||
import Media, { MediaOverlay } from 'react-md/lib/Media';
|
||||
import Dialog from 'react-md/lib/Dialogs';
|
||||
import TextField from 'react-md/lib/TextFields';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import InfoDrawer from '../common/InfoDrawer';
|
||||
|
@ -13,6 +17,10 @@ import ConfigurationStore from '../../stores/ConfigurationsStore';
|
|||
|
||||
interface IHomeState extends ISetupConfig {
|
||||
loaded?: boolean;
|
||||
templates?: IDashboardConfig[];
|
||||
selectedTemplateId?: string;
|
||||
template?: IDashboardConfig;
|
||||
creationState?: string;
|
||||
}
|
||||
|
||||
export default class Home extends React.Component<any, IHomeState> {
|
||||
|
@ -25,11 +33,35 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
redirectUrl: '',
|
||||
clientID: '',
|
||||
clientSecret: '',
|
||||
loaded: false
|
||||
loaded: false,
|
||||
|
||||
templates: [],
|
||||
selectedTemplateId: null,
|
||||
template: null,
|
||||
creationState: null
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.onNewTemplateSelected = this.onNewTemplateSelected.bind(this);
|
||||
this.onNewTemplateCancel = this.onNewTemplateCancel.bind(this);
|
||||
this.onNewTemplateSave = this.onNewTemplateSave.bind(this);
|
||||
|
||||
// Setting the state from the configuration store
|
||||
let state = ConfigurationStore.getState() || {} as any;
|
||||
let { templates, template, creationState } = state;
|
||||
this.state.templates = templates || [];
|
||||
this.state.template = template;
|
||||
this.state.creationState = creationState;
|
||||
|
||||
ConfigurationStore.listen(state => {
|
||||
this.setState({
|
||||
templates: state.templates || [] ,
|
||||
template: state.template,
|
||||
creationState: state.creationState
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -47,9 +79,51 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.state.creationState === 'successful') {
|
||||
window.location.replace('/dashboard/' + (this.refs.id as any).getField().value)
|
||||
}
|
||||
}
|
||||
|
||||
onNewTemplateSelected(templateId) {
|
||||
this.setState({ selectedTemplateId: templateId });
|
||||
ConfigurationActions.loadTemplate(templateId);
|
||||
}
|
||||
|
||||
onNewTemplateCancel() {
|
||||
this.setState({ selectedTemplateId: null });
|
||||
}
|
||||
|
||||
deepObjectExtend (target: any, source: any) {
|
||||
for (var prop in source)
|
||||
if (prop in target)
|
||||
this.deepObjectExtend(target[prop], source[prop]);
|
||||
else
|
||||
target[prop] = source[prop];
|
||||
return target;
|
||||
}
|
||||
|
||||
onNewTemplateSave() {
|
||||
|
||||
let createParams = {
|
||||
id: (this.refs.id as any).getField().value,
|
||||
name: (this.refs.name as any).getField().value,
|
||||
icon: (this.refs.icon as any).getField().value,
|
||||
url: (this.refs.id as any).getField().value
|
||||
};
|
||||
|
||||
var dashboard: IDashboardConfig = this.deepObjectExtend({}, this.state.template);
|
||||
dashboard.id = createParams.id;
|
||||
dashboard.name = createParams.name;
|
||||
dashboard.icon = createParams.icon;
|
||||
dashboard.url = createParams.url;
|
||||
|
||||
ConfigurationActions.createDashboard(dashboard);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let { admins, loaded, enableAuthentication, redirectUrl, clientID, clientSecret } = this.state;
|
||||
let { loaded, redirectUrl, templates, selectedTemplateId, template } = this.state;
|
||||
|
||||
if (!redirectUrl) {
|
||||
redirectUrl = window.location.protocol + '//' + window.location.host + '/auth/openid/return';
|
||||
|
@ -59,19 +133,62 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
return <CircularProgress key="progress" id="contentLoadingProgress" />;
|
||||
}
|
||||
|
||||
let templateCards = templates.map((template, index) => (
|
||||
<div style={{ maxWidth: 450, maxHeight: 216, margin: 10 }} className="md-cell--6" key={index}>
|
||||
<Card style={{ marginTop: 40 }}>
|
||||
<Media>
|
||||
<img src={template.preview} role="presentation" style={{ filter: 'opacity(30%)' }}/>
|
||||
<MediaOverlay>
|
||||
<CardTitle title={template.name} subtitle={template.description} wrapperStyle={{ whiteSpace: 'wrap' }}>
|
||||
<Button onClick={this.onNewTemplateSelected.bind(this, template.id)} className="md-cell--right" icon>add_circle_outline</Button>
|
||||
</CardTitle>
|
||||
</MediaOverlay>
|
||||
</Media>
|
||||
</Card>
|
||||
</div>
|
||||
))
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
Setup was completed. You can open one of the following dashboards...
|
||||
<Link href="/dashboard" to={null}>
|
||||
<a className='md-list-tile md-list-tile--mini' style={{width: '100%', overflow: 'hidden'}}>
|
||||
Dashboard
|
||||
</a>
|
||||
</Link>
|
||||
<Link href="/new-dashboard" to={null}>
|
||||
<a className='md-list-tile md-list-tile--mini' style={{width: '100%', overflow: 'hidden'}}>
|
||||
Create a new dashboard (Not active yet)
|
||||
</a>
|
||||
</Link>
|
||||
<div style={{ width: '100%' }} className="md-grid">
|
||||
{templateCards}
|
||||
|
||||
<Dialog
|
||||
id="configNewDashboard"
|
||||
visible={selectedTemplateId !== null && template !== null}
|
||||
title="Configure the new dashboard"
|
||||
aria-labelledby="configNewDashboardDescription"
|
||||
dialogStyle={{ width: '50%' }}
|
||||
modal
|
||||
actions={[
|
||||
{ onClick: this.onNewTemplateSave, primary: false, label: 'Create', },
|
||||
{ onClick: this.onNewTemplateCancel, primary: true, label: 'Cancel' }
|
||||
]}
|
||||
>
|
||||
<TextField
|
||||
id="id"
|
||||
ref="id"
|
||||
label="Dashboard Id"
|
||||
defaultValue={template && template.id || ''}
|
||||
lineDirection="center"
|
||||
placeholder="Choose an ID for the dashboard (will be used in the url)"
|
||||
/>
|
||||
<TextField
|
||||
id="name"
|
||||
ref="name"
|
||||
label="Dashboard Name"
|
||||
defaultValue={template && template.name || ''}
|
||||
lineDirection="center"
|
||||
placeholder="Choose name for the dashboard (will be used in navigation)"
|
||||
/>
|
||||
<TextField
|
||||
id="icon"
|
||||
ref="icon"
|
||||
label="Dashboard Icon"
|
||||
defaultValue={template && template.icon || 'dashboard'}
|
||||
lineDirection="center"
|
||||
placeholder="Choose icon for the dashboard (will be used in navigation)"
|
||||
/>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ import Chip from 'react-md/lib/Chips';
|
|||
import AccountStore from '../../stores/AccountStore';
|
||||
import AccountActions from '../../actions/AccountActions';
|
||||
|
||||
import ConfigurationsStore from '../../stores/ConfigurationsStore';
|
||||
|
||||
import './style.css';
|
||||
|
||||
const avatarSrc = 'https://cloud.githubusercontent.com/assets/13041/19686250/971bf7f8-9ac0-11e6-975c-188defd82df1.png';
|
||||
|
@ -47,9 +49,16 @@ export default class Navbar extends React.Component<any, any> {
|
|||
this.setState(state);
|
||||
});
|
||||
AccountActions.updateAccount();
|
||||
|
||||
ConfigurationsStore.listen((state) => {
|
||||
this.setState({
|
||||
dashboards: state.dashboards
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let { dashboards } = this.state;
|
||||
let { children, title } = this.props;
|
||||
let pathname = '/';
|
||||
try { pathname = window.location.pathname; } catch (e) { }
|
||||
|
@ -81,17 +90,6 @@ export default class Navbar extends React.Component<any, any> {
|
|||
<ListItem
|
||||
key="2"
|
||||
component={Link}
|
||||
href="/dashboard"
|
||||
active={pathname === '/dashboard'}
|
||||
leftIcon={<FontIcon>dashboard</FontIcon>}
|
||||
tileClassName="md-list-tile--mini"
|
||||
primaryText={'Dashboard'}
|
||||
/>
|
||||
),
|
||||
(
|
||||
<ListItem
|
||||
key="3"
|
||||
component={Link}
|
||||
href="/setup"
|
||||
active={pathname === '/setup'}
|
||||
leftIcon={<FontIcon>settings</FontIcon>}
|
||||
|
@ -101,6 +99,30 @@ export default class Navbar extends React.Component<any, any> {
|
|||
)
|
||||
];
|
||||
|
||||
(dashboards || []).forEach((dashboard, index) => {
|
||||
let name = dashboard.name || null;
|
||||
let url = '/dashboard/' + (dashboard.url || index.toString());
|
||||
let active = pathname === url;
|
||||
if (!title && active && name) {
|
||||
title = name;
|
||||
}
|
||||
|
||||
navigationItems.push(
|
||||
(
|
||||
<ListItem
|
||||
key={index + 4}
|
||||
component={Link}
|
||||
href={url}
|
||||
active={active}
|
||||
leftIcon={<FontIcon>{dashboard.icon || 'dashboard'}</FontIcon>}
|
||||
tileClassName="md-list-tile--mini"
|
||||
primaryText={name || 'Dashboard'}
|
||||
/>
|
||||
)
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
let toolbarActions =
|
||||
this.state.account ?
|
||||
<Chip style={{ marginRight: 30 }} label={'Hello, ' + this.state.account.displayName} /> :
|
||||
|
@ -129,6 +151,7 @@ export default class Navbar extends React.Component<any, any> {
|
|||
break;
|
||||
|
||||
default:
|
||||
|
||||
title = 'Ibex Dashboard';
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@ import * as React from 'react';
|
|||
import { GenericComponent, IGenericProps, IGenericState } from './GenericComponent';
|
||||
import * as moment from 'moment';
|
||||
import * as _ from 'lodash';
|
||||
import { AreaChart, Area as AreaFill, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, defs } from 'recharts';
|
||||
import { AreaChart, Area as AreaFill, XAxis, YAxis, CartesianGrid } from 'recharts';
|
||||
import { Tooltip, ResponsiveContainer, Legend, defs } from 'recharts';
|
||||
import Card from '../Card';
|
||||
import Switch from 'react-md/lib/SelectionControls/Switch';
|
||||
import './generic.css';
|
||||
|
@ -10,84 +11,104 @@ import colors from '../colors';
|
|||
var { ThemeColors } = colors;
|
||||
|
||||
interface IAreaProps extends IGenericProps {
|
||||
theme?: string[],
|
||||
showLegend?: boolean,
|
||||
isStacked?: boolean
|
||||
theme?: string[];
|
||||
showLegend?: boolean;
|
||||
isStacked?: boolean;
|
||||
}
|
||||
|
||||
interface IAreaState extends IGenericState {
|
||||
timeFormat: string,
|
||||
values: Object[],
|
||||
lines: Object[],
|
||||
isStacked: boolean
|
||||
timeFormat: string;
|
||||
values: Object[];
|
||||
lines: Object[];
|
||||
isStacked: boolean;
|
||||
}
|
||||
|
||||
export default class Area extends GenericComponent<IAreaProps, IAreaState> {
|
||||
|
||||
static defaultProps = {
|
||||
isStacked: true
|
||||
static defaultProps = {
|
||||
isStacked: true
|
||||
};
|
||||
|
||||
dateFormat(time: string) {
|
||||
return moment(time).format('MMM-DD');
|
||||
}
|
||||
|
||||
hourFormat(time: string) {
|
||||
return moment(time).format('HH:mm');
|
||||
}
|
||||
|
||||
generateWidgets() {
|
||||
let checked = this.is('isStacked');
|
||||
return (
|
||||
<div className="widgets">
|
||||
<Switch
|
||||
id="stack"
|
||||
name="stack"
|
||||
label="Stack"
|
||||
checked={checked}
|
||||
defaultChecked
|
||||
onChange={this.handleStackChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleStackChange = (checked) => {
|
||||
// NB: a render workaround is required when toggling stacked area view.
|
||||
this.setState({ isStacked: checked, values: this.state.values.slice() });
|
||||
}
|
||||
|
||||
render() {
|
||||
var { timeFormat, values, lines } = this.state;
|
||||
var { title, subtitle, theme, props } = this.props;
|
||||
var { showLegend, areaProps } = props;
|
||||
|
||||
var format = timeFormat === 'hour' ? this.hourFormat : this.dateFormat;
|
||||
var themeColors = theme || ThemeColors;
|
||||
|
||||
// gets the 'isStacked' boolean option from state, passed props or default values (in that order).
|
||||
var isStacked = this.is('isStacked');
|
||||
let stackProps = {};
|
||||
if (isStacked) {
|
||||
stackProps['stackId'] = '1';
|
||||
}
|
||||
|
||||
dateFormat(time) {
|
||||
return moment(time).format('MMM-DD');
|
||||
}
|
||||
var widgets = this.generateWidgets();
|
||||
|
||||
hourFormat(time) {
|
||||
return moment(time).format('HH:mm');
|
||||
}
|
||||
|
||||
generateWidgets() {
|
||||
let checked = this.is('isStacked');
|
||||
var fillElements = [];
|
||||
if (values && values.length && lines) {
|
||||
fillElements = lines.map((line, idx) => {
|
||||
return (
|
||||
<div className="widgets">
|
||||
<Switch id="stack" name="stack" label="Stack" checked={checked} defaultChecked onChange={this.handleStackChange} />
|
||||
</div>
|
||||
<AreaFill
|
||||
key={idx}
|
||||
dataKey={line}
|
||||
{...stackProps}
|
||||
type="monotone"
|
||||
stroke={themeColors[idx % themeColors.length]}
|
||||
fill={themeColors[idx % themeColors.length]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
handleStackChange = (checked) => {
|
||||
// NB: a render workaround is required when toggling stacked area view - cloning data values will cause the AreaChart to reanimate.
|
||||
this.setState({ isStacked: checked, values: this.state.values.slice() });
|
||||
}
|
||||
|
||||
render() {
|
||||
var { timeFormat, values, lines } = this.state;
|
||||
var { title, subtitle, theme, props } = this.props;
|
||||
var { showLegend, areaProps } = props;
|
||||
|
||||
var format = timeFormat === "hour" ? this.hourFormat : this.dateFormat;
|
||||
var themeColors = theme || ThemeColors;
|
||||
|
||||
// gets the 'isStacked' boolean option from state, passed props or default values (in that order).
|
||||
var isStacked = this.is('isStacked');
|
||||
let stackProps = {};
|
||||
if (isStacked) {
|
||||
stackProps['stackId'] = "1";
|
||||
}
|
||||
|
||||
var widgets = this.generateWidgets();
|
||||
|
||||
var fillElements = [];
|
||||
if (values && values.length && lines) {
|
||||
fillElements = lines.map((line, idx) => {
|
||||
return <AreaFill key={idx} dataKey={line} {...stackProps} type="monotone" stroke={themeColors[idx % themeColors.length]} fill={themeColors[idx % themeColors.length]} />
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={title} subtitle={subtitle}>
|
||||
{widgets}
|
||||
<ResponsiveContainer>
|
||||
<AreaChart ref="areaChart" margin={{ top: 5, right: 30, left: 20, bottom: 5 }} data={values} {...areaProps} >
|
||||
<XAxis dataKey="time" tickFormatter={format} minTickGap={20} />
|
||||
<YAxis />
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<Tooltip />
|
||||
{showLegend !== false && <Legend />}
|
||||
{fillElements}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card title={title} subtitle={subtitle}>
|
||||
{widgets}
|
||||
<ResponsiveContainer>
|
||||
<AreaChart
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
data={values}
|
||||
{...areaProps}
|
||||
>
|
||||
<XAxis dataKey="time" tickFormatter={format} minTickGap={20} />
|
||||
<YAxis />
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<Tooltip />
|
||||
{showLegend !== false && <Legend />}
|
||||
{fillElements}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -11,16 +11,16 @@ var { ThemeColors } = colors;
|
|||
|
||||
interface IBarProps extends IGenericProps {
|
||||
props: {
|
||||
barProps: { [key: string] : Object };
|
||||
barProps: { [key: string]: Object };
|
||||
showLegend: boolean;
|
||||
/** The name of the property in the data source that contains the name for the X axis */
|
||||
nameKey: string;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
interface IBarState extends IGenericState {
|
||||
values: Object[]
|
||||
bars: Object[]
|
||||
values: Object[];
|
||||
bars: Object[];
|
||||
}
|
||||
|
||||
export default class BarData extends GenericComponent<IBarProps, IBarState> {
|
||||
|
@ -28,18 +28,18 @@ export default class BarData extends GenericComponent<IBarProps, IBarState> {
|
|||
state = {
|
||||
values: [],
|
||||
bars: []
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick(data, index) {
|
||||
handleClick(data: any, index: number) {
|
||||
this.trigger('onBarClick', data.payload);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
var { values, bars } = this.state;
|
||||
var { title, subtitle, props } = this.props;
|
||||
|
@ -52,25 +52,27 @@ export default class BarData extends GenericComponent<IBarProps, IBarState> {
|
|||
var barElements = [];
|
||||
if (values && values.length && bars) {
|
||||
barElements = bars.map((bar, idx) => {
|
||||
return <Bar key={idx} dataKey={bar} fill={ThemeColors[idx]} onClick={this.handleClick} />
|
||||
})
|
||||
return <Bar key={idx} dataKey={bar} fill={ThemeColors[idx]} onClick={this.handleClick} />;
|
||||
});
|
||||
}
|
||||
|
||||
// Todo: Receive the width of the SVG component from the container
|
||||
return (
|
||||
<Card title={ title }
|
||||
subtitle={ subtitle }>
|
||||
<Card title={title} subtitle={subtitle}>
|
||||
<ResponsiveContainer>
|
||||
<BarChart data={values}
|
||||
margin={{top: 5, right: 30, left: 0, bottom: 5}}
|
||||
{...barProps}
|
||||
>
|
||||
<XAxis dataKey={ nameKey || '' }/>
|
||||
<YAxis/>
|
||||
<CartesianGrid strokeDasharray="3 3"/>
|
||||
<Tooltip/>
|
||||
{ barElements }
|
||||
{ showLegend !== false && <Legend layout="vertical" align="right" verticalAlign="top" wrapperStyle={{ right: 5 }} /> }
|
||||
<BarChart
|
||||
data={values}
|
||||
margin={{ top: 5, right: 30, left: 0, bottom: 5 }}
|
||||
{...barProps}
|
||||
>
|
||||
<XAxis dataKey={nameKey || ''} />
|
||||
<YAxis />
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<Tooltip />
|
||||
{barElements}
|
||||
{showLegend !== false &&
|
||||
<Legend layout="vertical" align="right" verticalAlign="top" wrapperStyle={{ right: 5 }} />
|
||||
}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as React from 'react';
|
|||
import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources';
|
||||
|
||||
export interface IGenericProps {
|
||||
id?: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
dependencies: { [key: string]: string };
|
||||
|
@ -20,9 +21,12 @@ export interface IGenericState { [key: string]: any; }
|
|||
export abstract class GenericComponent<T1 extends IGenericProps, T2 extends IGenericState>
|
||||
extends React.Component<T1, T2> {
|
||||
|
||||
private id: string = null;
|
||||
|
||||
constructor(props: T1) {
|
||||
super(props);
|
||||
|
||||
this.id = props.id || null;
|
||||
this.onStateChange = this.onStateChange.bind(this);
|
||||
this.trigger = this.trigger.bind(this);
|
||||
|
||||
|
@ -54,6 +58,19 @@ export abstract class GenericComponent<T1 extends IGenericProps, T2 extends IGen
|
|||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
||||
// This logic is used when the same id is used by two elements that appear in the same area.
|
||||
// Since they occupy the same id, componentWillMount/Unmount are not called since react
|
||||
// thinks the same component was updated.
|
||||
// Nonetheless, the properties may change and the element's dependencies may change.
|
||||
if (this.id !== this.props.id) {
|
||||
this.componentWillUnmount();
|
||||
this.componentDidMount();
|
||||
this.id = this.props.id;
|
||||
}
|
||||
}
|
||||
|
||||
protected trigger(actionName: string, args: IDictionary) {
|
||||
var action = this.props.actions[actionName];
|
||||
|
||||
|
@ -89,7 +106,6 @@ export abstract class GenericComponent<T1 extends IGenericProps, T2 extends IGen
|
|||
}
|
||||
|
||||
private onStateChange(state: any) {
|
||||
|
||||
var result = DataSourceConnector.extrapolateDependencies(this.props.dependencies);
|
||||
var updatedState: IGenericState = {};
|
||||
Object.keys(result.dependencies).forEach(key => {
|
||||
|
|
|
@ -126,9 +126,10 @@ export default class MenuFilter extends GenericComponent<any, any> {
|
|||
}
|
||||
|
||||
render() {
|
||||
var { title, subtitle, icon } = this.props;
|
||||
var { selectedValues, values, overlay } = this.state;
|
||||
const { title, subtitle, icon } = this.props;
|
||||
let { selectedValues, values, overlay } = this.state;
|
||||
values = values || [];
|
||||
selectedValues = selectedValues || [];
|
||||
let listItems = values.map((value, idx) => {
|
||||
return (
|
||||
<ListItemControl
|
||||
|
|
|
@ -10,21 +10,21 @@ import colors from '../colors';
|
|||
var { ThemeColors } = colors;
|
||||
|
||||
interface IPieProps extends IGenericProps {
|
||||
mode: string // users/messages
|
||||
mode: string; // users/messages
|
||||
props: {
|
||||
pieProps: { [key: string] : Object };
|
||||
pieProps: { [key: string]: Object };
|
||||
width: Object;
|
||||
height: Object;
|
||||
showLegend: boolean;
|
||||
legendVerticalAlign?: 'top' | 'bottom';
|
||||
compact?: boolean;
|
||||
}
|
||||
theme?: string[]
|
||||
};
|
||||
theme?: string[];
|
||||
};
|
||||
|
||||
interface IPieState extends IGenericState {
|
||||
activeIndex?: number,
|
||||
values?: Object[]
|
||||
activeIndex?: number;
|
||||
values?: Object[];
|
||||
}
|
||||
|
||||
export default class PieData extends GenericComponent<IPieProps, IPieState> {
|
||||
|
@ -32,15 +32,15 @@ export default class PieData extends GenericComponent<IPieProps, IPieState> {
|
|||
state = {
|
||||
activeIndex: 0,
|
||||
values: null
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
|
||||
this.onPieEnter = this.onPieEnter.bind(this);
|
||||
}
|
||||
|
||||
onPieEnter(data, index) {
|
||||
onPieEnter(data: any, index: number) {
|
||||
this.setState({ activeIndex: index });
|
||||
}
|
||||
|
||||
|
@ -62,29 +62,29 @@ export default class PieData extends GenericComponent<IPieProps, IPieState> {
|
|||
const ey = my;
|
||||
const textAnchor = cos >= 0 ? 'start' : 'end';
|
||||
|
||||
var c : any = {};
|
||||
var c: any = {};
|
||||
c.midAngle = 54.11764705882353;
|
||||
c.sin = Math.sin(-RADIAN * c.midAngle);
|
||||
c.cos = Math.cos(-RADIAN * c.midAngle);
|
||||
c.cx = cx;
|
||||
c.cy = cy;
|
||||
c.sx = cx + (outerRadius + 10) * c.cos;
|
||||
c.sy = cy + (outerRadius + 10) * c.sin;
|
||||
c.mx = cx + (outerRadius + 30) * c.cos;
|
||||
c.my = cy + (outerRadius + 30) * c.sin;
|
||||
c.ex = c.mx + (c.cos >= 0 ? 1 : -1) * 22;
|
||||
c.ey = c.my;
|
||||
c.textAnchor = 'start'
|
||||
c.sx = cx + (outerRadius + 10) * c.cos;
|
||||
c.sy = cy + (outerRadius + 10) * c.sin;
|
||||
c.mx = cx + (outerRadius + 30) * c.cos;
|
||||
c.my = cy + (outerRadius + 30) * c.sin;
|
||||
c.ex = c.mx + (c.cos >= 0 ? 1 : -1) * 22;
|
||||
c.ey = c.my;
|
||||
c.textAnchor = 'start';
|
||||
|
||||
return (
|
||||
<g>
|
||||
{ compact && [
|
||||
<text x={cx} y={cy} dy={-15} textAnchor="middle" fill={fill} style={{fontWeight: 500}}>{name}</text>,
|
||||
{compact && [
|
||||
<text x={cx} y={cy} dy={-15} textAnchor="middle" fill={fill} style={{ fontWeight: 500 }}>{name}</text>,
|
||||
<text x={cx} y={cy} dy={3} textAnchor="middle" fill={fill}>{`${value} ${type.toLowerCase()}`}</text>,
|
||||
<text x={cx} y={cy} dy={25} textAnchor="middle" fill="#999">{`(${(percent * 100).toFixed(2)}%)`}</text>
|
||||
] || [
|
||||
<text x={cx} y={cy} dy={8} textAnchor="middle" fill={fill}>{name}</text>,
|
||||
]}
|
||||
<text x={cx} y={cy} dy={8} textAnchor="middle" fill={fill}>{name}</text>,
|
||||
]}
|
||||
<Sector
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
|
@ -104,18 +104,24 @@ export default class PieData extends GenericComponent<IPieProps, IPieState> {
|
|||
fill={fill}
|
||||
/>
|
||||
|
||||
{ !compact && ([
|
||||
<path d={`M${c.sx},${c.sy}L${c.mx},${c.my}L${c.ex},${c.ey}`} stroke={fill} fill="none"/>,
|
||||
<circle cx={c.ex} cy={c.ey} r={2} fill={fill} stroke="none"/>,
|
||||
<text x={c.ex + (c.cos >= 0 ? 1 : -1) * 12} y={c.ey} textAnchor={c.textAnchor} fill="#333">{`${value} ${type.toLowerCase()}`}</text>,
|
||||
<text x={c.ex + (c.cos >= 0 ? 1 : -1) * 12} y={c.ey} dy={18} textAnchor={c.textAnchor} fill="#999">
|
||||
{`(Rate ${(percent * 100).toFixed(2)}%)`}
|
||||
</text>
|
||||
{!compact && ([
|
||||
<path d={`M${c.sx},${c.sy}L${c.mx},${c.my}L${c.ex},${c.ey}`} stroke={fill} fill="none" />,
|
||||
<circle cx={c.ex} cy={c.ey} r={2} fill={fill} stroke="none" />,
|
||||
(
|
||||
<text x={c.ex + (c.cos >= 0 ? 1 : -1) * 12} y={c.ey} textAnchor={c.textAnchor} fill="#333">
|
||||
{`${value} ${type.toLowerCase()}`}
|
||||
</text>
|
||||
),
|
||||
(
|
||||
<text x={c.ex + (c.cos >= 0 ? 1 : -1) * 12} y={c.ey} dy={18} textAnchor={c.textAnchor} fill="#999">
|
||||
{`(Rate ${(percent * 100).toFixed(2)}%)`}
|
||||
</text>
|
||||
)
|
||||
])}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
var { values } = this.state;
|
||||
var { props, title, subtitle, layout, theme } = this.props;
|
||||
|
@ -129,33 +135,31 @@ export default class PieData extends GenericComponent<IPieProps, IPieState> {
|
|||
|
||||
// Todo: Receive the width of the SVG component from the container
|
||||
return (
|
||||
<Card title={ title }
|
||||
subtitle={ subtitle }>
|
||||
<Card title={title} subtitle={subtitle}>
|
||||
<ResponsiveContainer>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={values}
|
||||
data={values}
|
||||
cx={Math.min(layout.h / 4, layout.w) * 70}
|
||||
innerRadius={60}
|
||||
fill="#8884d8"
|
||||
onMouseEnter={this.onPieEnter}
|
||||
activeIndex={this.state.activeIndex}
|
||||
activeShape={this.renderActiveShape.bind(this)}
|
||||
activeShape={this.renderActiveShape.bind(this)}
|
||||
paddingAngle={0}
|
||||
{...pieProps}>
|
||||
{
|
||||
values.map((entry, index) => <Cell key={index} fill={themeColors[index % themeColors.length]}/>)
|
||||
}
|
||||
<Cell key={0} fill={colors.GoodColor}/>
|
||||
<Cell key={1} fill={colors.BadColor}/>
|
||||
{...pieProps}
|
||||
>
|
||||
{values.map((entry, index) => <Cell key={index} fill={themeColors[index % themeColors.length]} />)}
|
||||
<Cell key={0} fill={colors.GoodColor} />
|
||||
<Cell key={1} fill={colors.BadColor} />
|
||||
</Pie>
|
||||
{
|
||||
showLegend !== false && (
|
||||
<Legend
|
||||
layout="vertical"
|
||||
align="right"
|
||||
verticalAlign={legendVerticalAlign || 'top'}
|
||||
wrapperStyle={{ paddingBottom: 10 }}
|
||||
<Legend
|
||||
layout="vertical"
|
||||
align="right"
|
||||
verticalAlign={legendVerticalAlign || 'top'}
|
||||
wrapperStyle={{ paddingBottom: 10 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,17 +2,18 @@ import * as React from 'react';
|
|||
import { GenericComponent, IGenericProps, IGenericState } from './GenericComponent';
|
||||
import * as moment from 'moment';
|
||||
import * as _ from 'lodash';
|
||||
import { ScatterChart, Scatter as ScatterLine, XAxis, YAxis, ZAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||
import { ScatterChart, Scatter as ScatterLine, XAxis, YAxis, ZAxis, CartesianGrid } from 'recharts';
|
||||
import { Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||
import Card from '../Card';
|
||||
import colors from '../colors';
|
||||
var { ThemeColors } = colors;
|
||||
|
||||
interface IScatterProps extends IGenericProps {
|
||||
theme?: string[],
|
||||
xDataKey?: string,
|
||||
yDataKey?: string,
|
||||
zDataKey?: string,
|
||||
zRange?: number[]
|
||||
theme?: string[];
|
||||
xDataKey?: string;
|
||||
yDataKey?: string;
|
||||
zDataKey?: string;
|
||||
zRange?: number[];
|
||||
}
|
||||
|
||||
interface IScatterState extends IGenericState {
|
||||
|
@ -22,11 +23,11 @@ interface IScatterState extends IGenericState {
|
|||
export default class Scatter extends GenericComponent<IScatterProps, IScatterState> {
|
||||
|
||||
static defaultProps = {
|
||||
xDataKey: "x",
|
||||
yDataKey: "y",
|
||||
zDataKey: "z",
|
||||
xDataKey: 'x',
|
||||
yDataKey: 'y',
|
||||
zDataKey: 'z',
|
||||
zRange: [10, 1000]
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
var { groupedValues } = this.state;
|
||||
|
@ -34,10 +35,10 @@ export default class Scatter extends GenericComponent<IScatterProps, IScatterSta
|
|||
var { scatterProps, groupTitles } = props;
|
||||
|
||||
var { xDataKey, yDataKey, zDataKey, zRange } = this.props.props;
|
||||
if (xDataKey === undefined) xDataKey = Scatter.defaultProps.xDataKey;
|
||||
if (yDataKey === undefined) yDataKey = Scatter.defaultProps.yDataKey;
|
||||
if (zDataKey === undefined) zDataKey = Scatter.defaultProps.zDataKey;
|
||||
if (zRange === undefined) zRange = Scatter.defaultProps.zRange;
|
||||
if (xDataKey === undefined) { xDataKey = Scatter.defaultProps.xDataKey; }
|
||||
if (yDataKey === undefined) { yDataKey = Scatter.defaultProps.yDataKey; }
|
||||
if (zDataKey === undefined) { zDataKey = Scatter.defaultProps.zDataKey; }
|
||||
if (zRange === undefined) { zRange = Scatter.defaultProps.zRange; }
|
||||
|
||||
var themeColors = theme || ThemeColors;
|
||||
|
||||
|
@ -49,7 +50,15 @@ export default class Scatter extends GenericComponent<IScatterProps, IScatterSta
|
|||
return;
|
||||
}
|
||||
let values = groupedValues[key];
|
||||
let line = <ScatterLine key={idx} name={key} data={values} fill={themeColors[idx % themeColors.length]} stroke={themeColors[idx % themeColors.length]} />
|
||||
let line = (
|
||||
<ScatterLine
|
||||
key={idx}
|
||||
name={key}
|
||||
data={values}
|
||||
fill={themeColors[idx % themeColors.length]}
|
||||
stroke={themeColors[idx % themeColors.length]}
|
||||
/>
|
||||
);
|
||||
scatterLines.push(line);
|
||||
idx += 1;
|
||||
});
|
||||
|
|
|
@ -1,42 +1,38 @@
|
|||
import * as React from 'react';
|
||||
import { GenericComponent } from './GenericComponent';
|
||||
import Button from 'react-md/lib/Buttons/Button';
|
||||
import SelectField from 'react-md/lib/SelectFields';
|
||||
|
||||
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
export default class TextFilter extends GenericComponent<any, any> {
|
||||
// static propTypes = {}
|
||||
// static defaultProps = {}
|
||||
|
||||
constructor(props) {
|
||||
static defaultProps = {
|
||||
title: 'Select'
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
onChange(newValue, index, event) {
|
||||
onChange(newValue: any) {
|
||||
this.trigger('onChange', newValue);
|
||||
}
|
||||
|
||||
render() {
|
||||
var { selectedValue, values } = this.state;
|
||||
var { title } = this.props;
|
||||
values = values || [];
|
||||
|
||||
// var buttons = values.map((value, idx) => {
|
||||
// return <Button flat key={idx} label={value} primary={value === selectedValue} onClick={this.onChange.bind(null, value)} />
|
||||
// })
|
||||
|
||||
return (
|
||||
<SelectField
|
||||
id="timespan"
|
||||
label="Timespan"
|
||||
label={title}
|
||||
value={selectedValue}
|
||||
menuItems={values}
|
||||
position={SelectField.Positions.BELOW}
|
||||
onChange={this.onChange}
|
||||
toolbar={false}
|
||||
className='md-select-field--toolbar'
|
||||
className="md-select-field--toolbar"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
|||
import { GenericComponent, IGenericProps, IGenericState } from './GenericComponent';
|
||||
import * as moment from 'moment';
|
||||
import * as _ from 'lodash';
|
||||
import {LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer} from 'recharts';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||
import Card from '../Card';
|
||||
|
||||
import Button from 'react-md/lib/Buttons/Button';
|
||||
|
@ -11,51 +11,58 @@ import colors from '../colors';
|
|||
var { ThemeColors } = colors;
|
||||
|
||||
interface ITimelineProps extends IGenericProps {
|
||||
theme?: string[]
|
||||
theme?: string[];
|
||||
}
|
||||
|
||||
interface ITimelineState extends IGenericState {
|
||||
timeFormat: string
|
||||
values: Object[]
|
||||
lines: Object[]
|
||||
timeFormat: string;
|
||||
values: Object[];
|
||||
lines: Object[];
|
||||
}
|
||||
|
||||
export default class Timeline extends GenericComponent<ITimelineProps, ITimelineState> {
|
||||
// static propTypes = {}
|
||||
// static defaultProps = {}
|
||||
|
||||
dateFormat (time) {
|
||||
dateFormat(time: string) {
|
||||
return moment(time).format('MMM-DD');
|
||||
}
|
||||
|
||||
hourFormat (time) {
|
||||
|
||||
hourFormat(time: string) {
|
||||
return moment(time).format('HH:mm');
|
||||
}
|
||||
|
||||
render() {
|
||||
var { timeFormat, values, lines } = this.state;
|
||||
var { title, subtitle, theme } = this.props;
|
||||
var { title, subtitle, theme, props } = this.props;
|
||||
var { lineProps } = props;
|
||||
|
||||
var format = timeFormat === "hour" ? this.hourFormat : this.dateFormat;
|
||||
var format = timeFormat === 'hour' ? this.hourFormat : this.dateFormat;
|
||||
var themeColors = theme || ThemeColors;
|
||||
|
||||
var lineElements = [];
|
||||
if (values && values.length && lines) {
|
||||
lineElements = lines.map((line, idx) => {
|
||||
return <Line key={idx} type="monotone" dataKey={line} stroke={themeColors[idx % themeColors.length]} dot={false} ticksCount={5}/>
|
||||
})
|
||||
return (
|
||||
<Line
|
||||
key={idx}
|
||||
type="monotone"
|
||||
dataKey={line}
|
||||
stroke={themeColors[idx % themeColors.length]}
|
||||
dot={false}
|
||||
ticksCount={5}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={ title }
|
||||
subtitle={ subtitle }>
|
||||
<Card title={title} subtitle={subtitle}>
|
||||
<ResponsiveContainer>
|
||||
<LineChart data={values} margin={{top: 5, right: 30, left: 20, bottom: 5}}>
|
||||
<XAxis dataKey="time" tickFormatter={format} minTickGap={20}/>
|
||||
<YAxis type="number" domain={['dataMin', 'dataMax']}/>
|
||||
<CartesianGrid strokeDasharray="3 3"/>
|
||||
<LineChart data={values} margin={{ top: 5, right: 30, left: 20, bottom: 5 }} {...lineProps}>
|
||||
<XAxis dataKey="time" tickFormatter={format} minTickGap={20} />
|
||||
<YAxis type="number" domain={['dataMin', 'dataMax']} />
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<Tooltip />
|
||||
<Legend/>
|
||||
<Legend />
|
||||
{lineElements}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
|
|
|
@ -3,22 +3,25 @@ import * as _ from 'lodash';
|
|||
import { IDataSourcePlugin } from './plugins/DataSourcePlugin';
|
||||
import DialogsActions from '../components/generic/Dialogs/DialogsActions';
|
||||
|
||||
import VisibilityActions from '../actions/VisibilityActions';
|
||||
import VisibilityStore from '../stores/VisibilityStore';
|
||||
|
||||
export interface IDataSource {
|
||||
id: string;
|
||||
config : any;
|
||||
plugin : IDataSourcePlugin;
|
||||
config: any;
|
||||
plugin: IDataSourcePlugin;
|
||||
action: any;
|
||||
store: any;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
export interface IDataSourceDictionary {
|
||||
[key: string] : IDataSource;
|
||||
[key: string]: IDataSource;
|
||||
}
|
||||
|
||||
export interface IExtrapolationResult {
|
||||
dataSources: { [key: string] : IDataSource };
|
||||
dependencies: { [key: string] : any };
|
||||
dataSources: { [key: string]: IDataSource };
|
||||
dependencies: { [key: string]: any };
|
||||
}
|
||||
|
||||
export class DataSourceConnector {
|
||||
|
@ -35,8 +38,8 @@ export class DataSourceConnector {
|
|||
// Dynamically load the plugin from the plugins directory
|
||||
var pluginPath = './plugins/' + config.type;
|
||||
var PluginClass = require(pluginPath);
|
||||
var plugin : any = new PluginClass.default(config, connections);
|
||||
|
||||
var plugin: any = new PluginClass.default(config, connections);
|
||||
|
||||
// Creating actions class
|
||||
var ActionClass = DataSourceConnector.createActionClass(plugin);
|
||||
|
||||
|
@ -50,7 +53,7 @@ export class DataSourceConnector {
|
|||
action: ActionClass,
|
||||
store: StoreClass,
|
||||
initialized: false
|
||||
}
|
||||
};
|
||||
|
||||
return DataSourceConnector.dataSources[config.id];
|
||||
}
|
||||
|
@ -83,11 +86,28 @@ export class DataSourceConnector {
|
|||
checkDS.action.updateDependencies.defer(state);
|
||||
}
|
||||
});
|
||||
|
||||
// Checking visibility flags
|
||||
let visibilityState = VisibilityStore.getState() || {};
|
||||
let flags = visibilityState.flags || {};
|
||||
let updatedFlags = {};
|
||||
let shouldUpdate = false;
|
||||
Object.keys(flags).forEach(visibilityKey => {
|
||||
let keyParts = visibilityKey.split(':');
|
||||
if (keyParts[0] === sourceDS.id) {
|
||||
updatedFlags[visibilityKey] = sourceDS.store.getState()[keyParts[1]];
|
||||
shouldUpdate = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldUpdate) {
|
||||
(<any>VisibilityActions.setFlags).defer(updatedFlags);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static initializeDataSources() {
|
||||
// Call initalize methods
|
||||
// Call initialize methods
|
||||
Object.keys(this.dataSources).forEach(sourceDSId => {
|
||||
var sourceDS = this.dataSources[sourceDSId];
|
||||
|
||||
|
@ -107,7 +127,7 @@ export class DataSourceConnector {
|
|||
dependencies: {}
|
||||
};
|
||||
Object.keys(dependencies).forEach(key => {
|
||||
|
||||
|
||||
// Find relevant store
|
||||
let dependency = dependencies[key] || '';
|
||||
|
||||
|
@ -131,7 +151,8 @@ export class DataSourceConnector {
|
|||
} else {
|
||||
let dataSource = DataSourceConnector.dataSources[dataSourceName];
|
||||
if (!dataSource) {
|
||||
throw new Error('Could not find data source for depedency ' + dependency + '. If your want to use a constant value, write "value:some value"');
|
||||
throw new Error(`Could not find data source for dependency ${dependency}.
|
||||
If your want to use a constant value, write "value:some value"`);
|
||||
}
|
||||
|
||||
let valueName = dependsUpon.length > 1 ? dependsUpon[1] : dataSource.plugin.defaultProperty;
|
||||
|
@ -142,6 +163,20 @@ export class DataSourceConnector {
|
|||
}
|
||||
});
|
||||
|
||||
// Checking to see if any of the dependencies control visibility
|
||||
let visibilityFlags = {};
|
||||
let updateVisibility = false;
|
||||
Object.keys(result.dependencies).forEach(key => {
|
||||
if (key === 'visible') {
|
||||
visibilityFlags[dependencies[key]] = result.dependencies[key];
|
||||
updateVisibility = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (updateVisibility) {
|
||||
(<any>VisibilityActions.setFlags).defer(visibilityFlags);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -154,14 +189,14 @@ export class DataSourceConnector {
|
|||
|
||||
var dataSourceName = actionLocation[0];
|
||||
var actionName = actionLocation[1];
|
||||
var selectedValuesProperty = "selectedValues";
|
||||
var selectedValuesProperty = 'selectedValues';
|
||||
if (actionLocation.length === 3) {
|
||||
selectedValuesProperty = actionLocation[2];
|
||||
args = { [selectedValuesProperty]: args };
|
||||
}
|
||||
|
||||
if (dataSourceName === 'dialog') {
|
||||
|
||||
|
||||
var extrapolation = DataSourceConnector.extrapolateDependencies(params, args);
|
||||
|
||||
DialogsActions.openDialog(actionName, extrapolation.dependencies);
|
||||
|
@ -169,7 +204,7 @@ export class DataSourceConnector {
|
|||
|
||||
var dataSource = DataSourceConnector.dataSources[dataSourceName];
|
||||
if (!dataSource) {
|
||||
throw new Error(`Data source ${dataSourceName} was not found`)
|
||||
throw new Error(`Data source ${dataSourceName} was not found`);
|
||||
}
|
||||
|
||||
dataSource.action[actionName].call(dataSource.action, args);
|
||||
|
@ -180,7 +215,7 @@ export class DataSourceConnector {
|
|||
return this.dataSources;
|
||||
}
|
||||
|
||||
static getDataSource(name): IDataSource {
|
||||
static getDataSource(name: string): IDataSource {
|
||||
return this.dataSources[name];
|
||||
}
|
||||
|
||||
|
@ -194,7 +229,7 @@ export class DataSourceConnector {
|
|||
if (typeof plugin[action] === 'function') {
|
||||
|
||||
// This method will be called with an action is dispatched
|
||||
NewActionClass.prototype[action] = function (...args) {
|
||||
NewActionClass.prototype[action] = function (...args: Array<any>) {
|
||||
// Collecting depedencies from all relevant stores
|
||||
var extrapolation;
|
||||
if (args.length === 1) {
|
||||
|
@ -209,26 +244,26 @@ export class DataSourceConnector {
|
|||
// Checking is result is a dispatcher or a direct value
|
||||
if (typeof result === 'function') {
|
||||
return (dispatch) => {
|
||||
result(function (obj) {
|
||||
result(function (obj: any) {
|
||||
obj = obj || {};
|
||||
var fullResult = DataSourceConnector.callibrateResult(obj, plugin);
|
||||
dispatch(fullResult);
|
||||
});
|
||||
}
|
||||
};
|
||||
} else {
|
||||
var fullResult = DataSourceConnector.callibrateResult(result, plugin);
|
||||
return fullResult;
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
|
||||
// Adding generic actions that are directly proxied to the store
|
||||
alt.addActions(action, <any>NewActionClass);
|
||||
alt.addActions(action, <any> NewActionClass);
|
||||
}
|
||||
});
|
||||
|
||||
// Binding the class to Alt and the plugin
|
||||
var ActionClass = alt.createActions(<any>NewActionClass);
|
||||
var ActionClass = alt.createActions(<any> NewActionClass);
|
||||
plugin.bind(ActionClass);
|
||||
|
||||
return ActionClass;
|
||||
|
@ -241,19 +276,18 @@ export class DataSourceConnector {
|
|||
});
|
||||
class NewStoreClass {
|
||||
constructor() {
|
||||
(<any>this).bindListeners({ updateState: bindings });
|
||||
(<any> this).bindListeners({ updateState: bindings });
|
||||
}
|
||||
|
||||
updateState(newData) {
|
||||
(<any>this).setState(newData);
|
||||
updateState(newData: any) {
|
||||
(<any> this).setState(newData);
|
||||
}
|
||||
};
|
||||
var StoreClass = alt.createStore(NewStoreClass, config.id + '-Store');;
|
||||
|
||||
}
|
||||
var StoreClass = alt.createStore(NewStoreClass, config.id + '-Store');
|
||||
return StoreClass;
|
||||
}
|
||||
|
||||
private static callibrateResult(result: any, plugin: IDataSourcePlugin) : any {
|
||||
private static callibrateResult(result: any, plugin: IDataSourcePlugin): any {
|
||||
|
||||
var defaultProperty = plugin.defaultProperty || 'value';
|
||||
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
|
||||
//import * as $ from 'jquery';
|
||||
import * as request from 'xhr-request';
|
||||
import * as _ from 'lodash';
|
||||
import { DataSourcePlugin, IOptions } from '../DataSourcePlugin';
|
||||
import { appInsightsUri } from './common';
|
||||
import ApplicationInsightsConnection from '../../connections/application-insights';
|
||||
import {DataSourceConnector} from '../../DataSourceConnector';
|
||||
|
||||
let connectionType = new ApplicationInsightsConnection();
|
||||
|
||||
interface IQueryParams {
|
||||
query?: ((dependencies: any) => string) | string;
|
||||
mappings?: (string|object)[];
|
||||
mappings?: (string | object)[];
|
||||
table?: string;
|
||||
queries?: IDictionary;
|
||||
filters?: Array<IFilterParams>;
|
||||
|
@ -36,7 +35,7 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
|
||||
var props = this._props;
|
||||
var params = props.params;
|
||||
|
||||
|
||||
// Validating params
|
||||
this.validateParams(props, params);
|
||||
}
|
||||
|
@ -46,7 +45,7 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
* @param {object} dependencies
|
||||
* @param {function} callback
|
||||
*/
|
||||
updateDependencies(dependencies) {
|
||||
updateDependencies(dependencies: any) {
|
||||
var emptyDependency = _.find(_.keys(this._props.dependencies), dependencyKey => {
|
||||
return typeof dependencies[dependencyKey] === 'undefined';
|
||||
});
|
||||
|
@ -81,18 +80,18 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
|
||||
if (!isForked) {
|
||||
let queryKey = this._props.id;
|
||||
query = this.compileQueryWithFilters(params.query, dependencies, isForked, queryKey, filters);
|
||||
query = this.query(params.query, dependencies, isForked, queryKey, filters);
|
||||
mappings.push(params.mappings);
|
||||
} else {
|
||||
queries = params.queries || {};
|
||||
table = params.table;
|
||||
query = ` ${params.table} | fork `;
|
||||
query = ` ${table} | fork `;
|
||||
_.keys(queries).every(queryKey => {
|
||||
let queryParams = queries[queryKey];
|
||||
filters = queryParams.filters || [];
|
||||
tableNames.push(queryKey);
|
||||
mappings.push(queryParams.mappings);
|
||||
query += this.compileQueryWithFilters(queryParams.query, dependencies, isForked, queryKey, filters);
|
||||
query += this.query(queryParams.query, dependencies, isForked, queryKey, filters);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
@ -104,12 +103,12 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
return (dispatch) => {
|
||||
|
||||
request(url, {
|
||||
method: "GET",
|
||||
method: 'GET',
|
||||
json: true,
|
||||
headers: {
|
||||
"x-api-key": apiKey
|
||||
'x-api-key': apiKey
|
||||
}
|
||||
}, (error, json) => {
|
||||
}, (error, json) => {
|
||||
|
||||
if (error) {
|
||||
return this.failure(error);
|
||||
|
@ -125,7 +124,7 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
}
|
||||
|
||||
// Map tables to appropriate results
|
||||
var resultTables = tables.filter((table, idx) => {
|
||||
var resultTables = tables.filter((aTable, idx) => {
|
||||
return idx < resultStatus.length && resultStatus[idx].Kind === 'QueryResult';
|
||||
});
|
||||
|
||||
|
@ -133,27 +132,28 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
values: (resultTables.length && resultTables[0]) || null
|
||||
};
|
||||
|
||||
tableNames.forEach((table: string, idx: number) => {
|
||||
returnedResults[table] = resultTables.length > idx ? resultTables[idx] : null;
|
||||
|
||||
tableNames.forEach((aTable: string, idx: number) => {
|
||||
returnedResults[aTable] = resultTables.length > idx ? resultTables[idx] : null;
|
||||
// Get state for filter selection
|
||||
const prevState = DataSourceConnector.getDataSource(this._props.id).store.getState();
|
||||
// Extracting calculated values
|
||||
let calc = queries[table].calculated;
|
||||
let calc = queries[aTable].calculated;
|
||||
if (typeof calc === 'function') {
|
||||
var additionalValues = calc(returnedResults[table], dependencies) || {};
|
||||
var additionalValues = calc(returnedResults[aTable], dependencies, prevState) || {};
|
||||
_.extend(returnedResults, additionalValues);
|
||||
}
|
||||
});
|
||||
|
||||
return dispatch(returnedResults);
|
||||
return dispatch(returnedResults);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
updateSelectedValues(dependencies: IDictionary, selectedValues: any) {
|
||||
if ( Array.isArray(selectedValues) ){
|
||||
return _.extend(dependencies, {"selectedValues":selectedValues});
|
||||
if (Array.isArray(selectedValues)) {
|
||||
return _.extend(dependencies, { 'selectedValues': selectedValues });
|
||||
}
|
||||
return _.extend(dependencies, selectedValues);
|
||||
return _.extend(dependencies, { ...selectedValues });
|
||||
}
|
||||
|
||||
private mapAllTables(results: IQueryResults, mappings: Array<IDictionary>): any[][] {
|
||||
|
@ -182,8 +182,8 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
|
||||
// Going over user defined mappings of the values
|
||||
_.keys(mappings).forEach(col => {
|
||||
row[col] =
|
||||
typeof mappings[col] === 'function' ?
|
||||
row[col] =
|
||||
typeof mappings[col] === 'function' ?
|
||||
mappings[col](row[col], row, rowIdx) :
|
||||
mappings[col];
|
||||
});
|
||||
|
@ -196,10 +196,10 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
return typeof query === 'function' ? query(dependencies) : query;
|
||||
}
|
||||
|
||||
private compileQueryWithFilters(query: any, dependencies: any, isForked: boolean, queryKey: string, filters: IFilterParams[]): string {
|
||||
private query(query: any, dependencies: any, isForked: boolean, queryKey: string, filters: IFilterParams[]): string {
|
||||
let q = this.compileQuery(query, dependencies);
|
||||
// Don't filter a filter query, or no filters specified
|
||||
if (queryKey.startsWith("filter") || filters === undefined || filters.length === 0) {
|
||||
if (queryKey.startsWith('filter') || filters === undefined || filters.length === 0) {
|
||||
return this.formatQuery(q, isForked);
|
||||
}
|
||||
// Apply selected filters to connected query
|
||||
|
@ -207,8 +207,8 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
const { dependency, queryProperty } = filter;
|
||||
const selectedFilters = dependencies[dependency] || [];
|
||||
if (selectedFilters.length > 0) {
|
||||
const filter = "where " + selectedFilters.map((value) => `${queryProperty}=="${value}"`).join(' or ') + " | ";
|
||||
q = ` ${filter} \n ${q} `;
|
||||
const f = 'where ' + selectedFilters.map((value) => `${queryProperty}=="${value}"`).join(' or ') + ' | ';
|
||||
q = ` ${f} \n ${q} `;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -237,7 +237,9 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
|
||||
if (params.table) {
|
||||
if (!params.queries) {
|
||||
return this.failure(new Error('Application Insights query should either have { query } or { table, queries } under params.'));
|
||||
return this.failure(
|
||||
new Error('Application Insights query should either have { query } or { table, queries } under params.')
|
||||
);
|
||||
}
|
||||
if (typeof params.table !== 'string' || typeof params.queries !== 'object' || Array.isArray(params.queries)) {
|
||||
throw new Error('{ table, queries } should be of types { "string", { query1: {...}, query2: {...} } }.');
|
||||
|
|
|
@ -22,7 +22,7 @@ export default class Constant extends DataSourcePlugin<IConstantParams> {
|
|||
}
|
||||
|
||||
initialize() {
|
||||
var { selectedValue, values } = <any> this._props.params;
|
||||
var { selectedValue, values, fla } = <any> this._props.params;
|
||||
return { selectedValue, values };
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ export interface IDataSourceOptions {
|
|||
}
|
||||
|
||||
export interface ICalculated {
|
||||
[key: string]: (state: Object, dependencies: IDictionary) => any;
|
||||
[key: string]: (state: Object, dependencies: IDictionary, prevState: Object) => any;
|
||||
}
|
||||
|
||||
export interface IOptions<T> {
|
||||
|
@ -50,7 +50,7 @@ export abstract class DataSourcePlugin<T> implements IDataSourcePlugin {
|
|||
dependencies: {} as any,
|
||||
dependables: [],
|
||||
actions: [ 'updateDependencies', 'failure', 'updateSelectedValues' ],
|
||||
params: <T>{},
|
||||
params: <T> {},
|
||||
calculated: {}
|
||||
};
|
||||
|
||||
|
@ -64,11 +64,12 @@ export abstract class DataSourcePlugin<T> implements IDataSourcePlugin {
|
|||
props.dependencies = options.dependencies || [];
|
||||
props.dependables = options.dependables || [];
|
||||
props.actions.push.apply(props.actions, options.actions || []);
|
||||
props.params = <T>(options.params || {});
|
||||
props.params = <T> (options.params || {});
|
||||
props.calculated = options.calculated || {};
|
||||
|
||||
this.updateDependencies = this.updateDependencies.bind(this);
|
||||
this.updateSelectedValues = this.updateSelectedValues.bind(this);
|
||||
this.getCalculated = this.getCalculated.bind(this);
|
||||
}
|
||||
|
||||
abstract updateDependencies (dependencies: IDictionary, args: IDictionary, callback: (result: any) => void): void;
|
||||
|
|
|
@ -22,7 +22,7 @@ export default class Config extends React.Component<any, IDashboardState> {
|
|||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
ConfigurationsActions.loadConfiguration();
|
||||
//ConfigurationsActions.loadConfiguration();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
|
|
@ -23,7 +23,7 @@ export default class Dashboard extends React.Component<any, IDashboardState> {
|
|||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
ConfigurationsActions.loadConfiguration();
|
||||
// ConfigurationsActions.loadConfiguration();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
|
|
@ -16,6 +16,7 @@ export default (
|
|||
<Route path="/about" component={About} />
|
||||
<Route path="/dashboard" component={Dashboard} />
|
||||
<Route path="/dashboard/config" component={Config} />
|
||||
<Route path="/dashboard/:id" component={Dashboard}/>
|
||||
<Route path="/setup" component={Setup} />
|
||||
<Route path="*" component={NotFound} />
|
||||
</Route>
|
||||
|
|
|
@ -7,6 +7,10 @@ import configurationActions from '../actions/ConfigurationsActions';
|
|||
|
||||
interface IConfigurationsStoreState {
|
||||
dashboard: IDashboardConfig;
|
||||
dashboards: IDashboardConfig[];
|
||||
template: IDashboardConfig;
|
||||
templates: IDashboardConfig[];
|
||||
creationState: string;
|
||||
connections: IDictionary;
|
||||
connectionsMissing: boolean;
|
||||
loaded: boolean;
|
||||
|
@ -15,6 +19,10 @@ interface IConfigurationsStoreState {
|
|||
class ConfigurationsStore extends AbstractStoreModel<IConfigurationsStoreState> implements IConfigurationsStoreState {
|
||||
|
||||
dashboard: IDashboardConfig;
|
||||
dashboards: IDashboardConfig[];
|
||||
template: IDashboardConfig;
|
||||
templates: IDashboardConfig[];
|
||||
creationState: string;
|
||||
connections: IDictionary;
|
||||
connectionsMissing: boolean;
|
||||
loaded: boolean;
|
||||
|
@ -23,16 +31,42 @@ class ConfigurationsStore extends AbstractStoreModel<IConfigurationsStoreState>
|
|||
super();
|
||||
|
||||
this.dashboard = null;
|
||||
this.dashboards = null;
|
||||
this.template = null;
|
||||
this.templates = null;
|
||||
this.creationState = null;
|
||||
this.connections = {};
|
||||
this.connectionsMissing = false;
|
||||
this.loaded = false;
|
||||
|
||||
this.bindListeners({
|
||||
loadConfiguration: configurationActions.loadConfiguration
|
||||
loadConfiguration: configurationActions.loadConfiguration,
|
||||
loadDashboard: configurationActions.loadDashboard,
|
||||
loadTemplate: configurationActions.loadTemplate,
|
||||
createDashboard: configurationActions.createDashboard
|
||||
});
|
||||
|
||||
configurationActions.loadConfiguration();
|
||||
|
||||
let pathname = window.location.pathname;
|
||||
if (pathname === '/dashboard') {
|
||||
configurationActions.loadDashboard("0");
|
||||
}
|
||||
|
||||
if (pathname.startsWith('/dashboard/')) {
|
||||
let dashboardId = pathname.substring('/dashboard/'.length);
|
||||
configurationActions.loadDashboard(dashboardId);
|
||||
}
|
||||
}
|
||||
|
||||
loadConfiguration(dashboard: IDashboardConfig) {
|
||||
loadConfiguration(result: { dashboards: IDashboardConfig[], templates: IDashboardConfig[] }) {
|
||||
let { dashboards,templates } = result;
|
||||
this.dashboards = dashboards;
|
||||
this.templates = templates;
|
||||
}
|
||||
|
||||
loadDashboard(result: { dashboard: IDashboardConfig }) {
|
||||
let { dashboard } = result;
|
||||
this.dashboard = dashboard;
|
||||
|
||||
if (this.dashboard && !this.loaded) {
|
||||
|
@ -44,6 +78,26 @@ class ConfigurationsStore extends AbstractStoreModel<IConfigurationsStoreState>
|
|||
this.connectionsMissing = Object.keys(this.connections).some(connectionKey => {
|
||||
var connection = this.connections[connectionKey];
|
||||
|
||||
return Object.keys(connection).some(paramKey => !connection[paramKey]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createDashboard(result: { dashboard: IDashboardConfig }) {
|
||||
this.creationState = 'successful';
|
||||
}
|
||||
|
||||
loadTemplate(result: { template: IDashboardConfig }) {
|
||||
let { template } = result;
|
||||
this.template = template;
|
||||
|
||||
if (this.template) {
|
||||
|
||||
this.connections = this.getConnections(template);
|
||||
|
||||
// Checking for missing connection params
|
||||
this.connectionsMissing = Object.keys(this.connections).some(connectionKey => {
|
||||
var connection = this.connections[connectionKey];
|
||||
return Object.keys(connection).some(paramKey => !connection[paramKey]);
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import alt, { AbstractStoreModel } from '../alt';
|
||||
|
||||
import visibilityActions from '../actions/VisibilityActions';
|
||||
|
||||
class VisibilityStore extends AbstractStoreModel<any> {
|
||||
|
||||
flags: IDict<boolean>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.flags = {};
|
||||
|
||||
this.bindListeners({
|
||||
updateFlags: [visibilityActions.turnFlagOn, visibilityActions.turnFlagOff, visibilityActions.setFlags]
|
||||
});
|
||||
}
|
||||
|
||||
updateFlags(flags: any) {
|
||||
if (flags) {
|
||||
Object.keys(flags).forEach(flag => {
|
||||
this.flags[flag] = flags[flag];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const visibilityStore = alt.createStore<any>(VisibilityStore, 'VisibilityStore');
|
||||
|
||||
export default visibilityStore;
|
|
@ -6,6 +6,12 @@ type IConnection = IStringDictionary;
|
|||
type IConnections = IDict<IConnection>;
|
||||
|
||||
interface IDashboardConfig extends IDataSourceContainer, IElementsContainer {
|
||||
id: string,
|
||||
name: string,
|
||||
icon: string,
|
||||
url: string,
|
||||
description?: string,
|
||||
preview?: string,
|
||||
config: {
|
||||
connections: IConnections,
|
||||
layout: {
|
||||
|
@ -38,7 +44,7 @@ interface IDataSource {
|
|||
type: string
|
||||
dependencies?: { [id: string]: string }
|
||||
params?: { [id: string]: any }
|
||||
calculated?: (state, dependencies) => { [index: string]: any }
|
||||
calculated?: (state, dependencies, prevState) => { [index: string]: any }
|
||||
}
|
||||
|
||||
interface IElement {
|
||||
|
|
Загрузка…
Ссылка в новой задаче