This commit is contained in:
David Douglas 2017-05-02 10:48:02 -04:00
Родитель c978ff1ac0 c43bf9930f
Коммит 8eeca1bf76
42 изменённых файлов: 2571 добавлений и 370 удалений

4
.vscode/launch.json поставляемый
Просмотреть файл

@ -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
.vscode/settings.json поставляемый
Просмотреть файл

@ -5,5 +5,8 @@
},
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.detectIndentation": false
"editor.detectIndentation": false,
"editor.rulers": [
120
],
}

Просмотреть файл

@ -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"
}
}
```

79
docs/bar.md Normal file
Просмотреть файл

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

53
docs/pie.md Normal file
Просмотреть файл

@ -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.

55
docs/scatter.md Normal file
Просмотреть файл

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

54
docs/timeline.md Normal file
Просмотреть файл

@ -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"
}
}
```

72
docs/two-modes-element.md Normal file
Просмотреть файл

@ -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`.

Двоичные данные
public/images/bot-framework-preview.png Normal file

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

После

Ширина:  |  Высота:  |  Размер: 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;

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

@ -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 {