This commit is contained in:
David Douglas 2017-05-22 10:23:26 +01:00
Родитель 23e9ac3a8a 36997c502d
Коммит 3eceafc49f
76 изменённых файлов: 1679 добавлений и 981 удалений

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

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

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

@ -17,7 +17,8 @@
"node-sass": "^4.5.0",
"npm-run-all": "^4.0.2",
"react-addons-test-utils": "^15.4.2",
"react-scripts-ts": "1.1.6"
"react-scripts-ts": "1.1.6",
"tslint": "^4.0.0"
},
"dependencies": {
"alt": "^0.18.6",
@ -33,6 +34,7 @@
"material-colors": "^1.2.5",
"moment": "^2.18.0",
"morgan": "^1.8.1",
"ms-rest-azure": "^2.1.2",
"passport": "^0.3.2",
"passport-azure-ad": "^3.0.5",
"react": "^15.4.2",

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

@ -9,6 +9,7 @@ const bodyParser = require('body-parser');
const authRouter = require('./routes/auth');
const apiRouter = require('./routes/api');
const cosmosDBRouter = require('./routes/cosmos-db');
const azureRouter = require('./routes/azure');
const app = express();
app.use(cookieParser());
@ -23,6 +24,7 @@ app.use(authRouter.authenticationMiddleware('/auth', '/api/setup'));
app.use('/auth', authRouter.router);
app.use('/api', apiRouter.router);
app.use('/cosmosdb', cosmosDBRouter.router);
app.use('/azure', azureRouter.router);
app.use(express.static(path.resolve(__dirname, '..', 'build')));

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

@ -0,0 +1,89 @@
/// <reference path="../../../src/types.d.ts"/>
import * as _ from 'lodash';
// The following line is important to keep in that format so it can be rendered into the page
export const config: IDashboardConfig = /*return*/ {
id: "azure_sample",
name: "Azure Sample",
icon: "dashboard",
url: "azure_sample",
description: "A basic azure sample to get connected to resources",
preview: "/images/bot-framework-preview.png",
html: `Azure sample dashboard`,
config: {
connections: { },
layout: {
isDraggable: true,
isResizable: true,
rowHeight: 30,
verticalCompact: false,
cols: { lg: 12,md: 10,sm: 6,xs: 4,xxs: 2 },
breakpoints: { lg: 1200,md: 996,sm: 768,xs: 480,xxs: 0 }
}
},
dataSources: [
{
id: "samples",
type: "Sample",
params: {
samples: {
initialValue: 0
}
}
},
{
id: "azure",
type: "Azure",
dependencies: { someValue: "samples:initialValue" },
params: { type: 'resources' },
calculated: (state, dependencies) => {
console.log(state);
let resources = state.values || [];
let resourceTypes = _.toPairs(_.groupBy(state.values, 'kind')).map(val => ({ name: val[0], value: val[1].length}));
return { resources, resourceTypes };
}
},
{
id: "azureLocations",
type: "Azure",
dependencies: { someValue: "samples:initialValue" },
params: { type: 'locations' },
calculated: (state, dependencies) => {
console.log(state);
let locations = state.values || [];
let mapData = locations.map(loc => ({
lat: loc.latitude,
lng: loc.longitude,
tooltip: loc.displayName + ': ' + loc.name
}));
return { locations: mapData };
}
}
],
filters: [],
elements: [
{
id: "pie_sample1",
type: "PieData",
title: "Pie Sample 1",
subtitle: "Description of pie sample 1",
size: { w: 5,h: 8 },
dependencies: { values: "azure:resourceTypes" },
props: { showLegend: true }
},
{
id: 'locations_map',
type: 'MapData',
title: "Locations Distribution",
subtitle: "Monitor regional activity",
size: { w: 7,h: 12 },
dependencies: { locations: "azureLocations:locations" },
props: { mapProps: { zoom: 1,maxZoom: 6 } }
}
],
dialogs: []
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -1,7 +1,7 @@
return {
id: "basic_sample",
name: "Basic Sample",
icon: "dashboard",
icon: "extension",
url: "basic_sample",
description: "A basic sample to see how data is connected to graphs",
preview: "/images/bot-framework-preview.png",

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

@ -19,6 +19,7 @@ const fields = {
name: /\s*name:\s*("|')(.*)("|')/,
description: /\s*description:\s*("|')(.*)("|')/,
icon: /\s*icon:\s*("|')(.*)("|')/,
logo: /\s*logo:\s*("|')(.*)("|')/,
url: /\s*url:\s*("|')(.*)("|')/,
preview: /\s*preview:\s*("|')(.*)("|')/,
html: /\s*html:\s*(`)([\s\S]*?)(`)/gm

36
server/routes/azure.js Normal file
Просмотреть файл

@ -0,0 +1,36 @@
const fs = require('fs');
const path = require('path');
const express = require('express');
const msRestAzure = require('ms-rest-azure');
const AzureServiceClient = msRestAzure.AzureServiceClient;
const router = new express.Router();
router.post('/query', (req, res) => {
let { servicePrincipalId, servicePrincipalKey, servicePrincipalDomain, subscriptionId, options } = req.body || { };
// Interactive Login
msRestAzure.loginWithServicePrincipalSecret(servicePrincipalId, servicePrincipalKey, servicePrincipalDomain, function(err, credentials) {
if (err) { return this.failure(err); }
let client = new AzureServiceClient(credentials, null);
options.method = options.method || 'GET';
options.url = `https://management.azure.com` + options.url;
return client.sendRequest(options, (err, result) => {
if (err) { throw err; }
let values = result.value || [];
return res.json(values);
});
});
});
module.exports = {
router
}

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

@ -15,11 +15,7 @@ class AccountActions extends AbstractActions implements IAccountActions {
return (dispatcher: (account: IDictionary) => void) => {
request('/auth/account', {
json: true
},
(error: any, result: any) => {
request('/auth/account', { json: true }, (error: any, result: any) => {
if (error) {
return this.failure(error);
}

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

@ -57,7 +57,7 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
json: true,
body: { script: 'return ' + script }
},
(error: any, json: any) => {
(error: any, json: any) => {
if (error || (json && json.errors)) {
return this.failure(error || json.errors);
@ -95,7 +95,7 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
json: true,
body: { script: 'return ' + stringDashboard }
},
(error: any, json: any) => {
(error: any, json: any) => {
if (error) {
return this.failure(error);
@ -111,11 +111,16 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
return { error };
}
private getScript(source: string, callback?: () => void): void {
private getScript(source: string, callback?: () => void): boolean {
let script: any = document.createElement('script');
let prior = document.getElementsByTagName('script')[0];
script.async = 1;
prior.parentNode.insertBefore(script, prior);
if (prior) {
prior.parentNode.insertBefore(script, prior);
} else {
document.getElementsByTagName('body')[0].appendChild(script);
}
script.onload = script.onreadystatechange = (_, isAbort) => {
if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState) ) {
@ -127,6 +132,7 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
};
script.src = source;
return true;
}
/**
@ -135,13 +141,15 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
*/
private objectToString(obj: Object, indent: number = 0, lf: boolean = false): string {
let result = ''; //(lf ? '\n' : '') + '\t'.repeat(indent);
let result = ''; // (lf ? '\n' : '') + '\t'.repeat(indent);
let sind = '\t'.repeat(indent);
let objectType = (Array.isArray(obj) && 'array') || typeof obj;
switch (objectType) {
case 'object': {
if (obj === null) { return result = 'null'; }
// Iterating through all values in object
let objectValue = '';
let objectValues = [];
@ -258,7 +266,7 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
calculated = calculated.substr('function(){return'.length, calculated.length - 'function(){return'.length - 1);
eval('dataSource.calculated = ' + calculated); /* tslint:disable-line */
}
})
});
}
}

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

@ -17,9 +17,7 @@ class SetupActions extends AbstractActions implements ISetupActions {
return (dispatcher: (setupConfig: ISetupConfig) => void) => {
request('/api/setup', { json: true },
(error: any, setupConfig: ISetupConfig) => {
request('/api/setup', { json: true }, (setupError: any, setupConfig: ISetupConfig) => {
return dispatcher(setupConfig);
});
};
@ -36,20 +34,20 @@ class SetupActions extends AbstractActions implements ISetupActions {
json: true,
body: { json: stringConfig }
},
(error: any, json: any) => {
(setupError: any, setupJson: any) => {
if (error) {
return this.failure(error);
if (setupError) {
return this.failure(setupError);
}
return request('/auth/init',
(error: any, json: any) => {
(authError: any, authJson: any) => {
if (error) {
return this.failure(error);
if (authError) {
return this.failure(authError);
}
let toast : IToast = { text: 'Setup was saved successfully.' };
let toast: IToast = { text: 'Setup was saved successfully.' };
ToastActions.addToast(toast);
try {
@ -58,7 +56,7 @@ class SetupActions extends AbstractActions implements ISetupActions {
}
} catch (e) { }
return dispatcher(json);
return dispatcher(authJson);
}
);
}

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

@ -4,24 +4,27 @@ import { Card, CardTitle } from 'react-md/lib/Cards';
import TooltipFontIcon from './TooltipFontIcon';
import Button from 'react-md/lib/Buttons';
export default ({children = null, title = '', subtitle = ''}) => (
<Card>
<CardTitle title={''} subtitle={[
<span key={0}>{title}</span>,
<TooltipFontIcon
key={1}
tooltipLabel={subtitle}
tooltipPosition="top"
forceIconFontSize={true}
forceIconSize={16}
className="card-icon"
>
info
</TooltipFontIcon>
]} />
<Media>
{children}
</Media>
</Card>
export default ({children = null, title = '', subtitle = ''}) => {
const titleNode = <span key={0}>{title}</span>;
const tooltipNode = (
<TooltipFontIcon
key={1}
tooltipLabel={subtitle}
tooltipPosition="top"
forceIconFontSize={true}
forceIconSize={16}
className="card-icon"
>
info
</TooltipFontIcon>
);
);
return (
<Card>
<CardTitle title={''} subtitle={[titleNode, tooltipNode]} />
<Media>
{children}
</Media>
</Card>
);
};

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

@ -1,27 +0,0 @@
import * as React from 'react';
import { Media } from 'react-md/lib/Media';
import { Card, CardTitle } from 'react-md/lib/Cards';
import TooltipFontIcon from './TooltipFontIcon';
import Button from 'react-md/lib/Buttons';
export default ({children = null, title = '', subtitle = ''}) => (
<Card>
<CardTitle title={''} subtitle={[
<span key={0}>{title}</span>,
<TooltipFontIcon
key={1}
tooltipLabel={subtitle}
tooltipPosition="top"
forceIconFontSize={true}
forceIconSize={16}
className="card-icon"
>
info
</TooltipFontIcon>
]} />
<Media aspectRatio="1-1" style={{ width: '100%', height: 'calc(100% - 45px)', marginTop: '0px' }}>
{children}
</Media>
</Card>
);

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

@ -272,7 +272,8 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
if (prevSection !== '') {
downloadItems.push(<Divider key={item.source + '_' + index} className="md-cell md-cell--12" />);
}
downloadItems.push(<Subheader primaryText={item.source} key={item.source + index} className="md-cell md-cell--12" />);
downloadItems.push(
<Subheader primaryText={item.source} key={item.source + index} className="md-cell md-cell--12" />);
}
downloadItems.push(
<ListItem

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

@ -4,6 +4,7 @@ export default class Help extends React.Component<any, any> {
render() {
// tslint:disable:max-line-length
return (
<div>
<h2>Background</h2>
@ -29,5 +30,6 @@ export default class Help extends React.Component<any, any> {
</p>
</div>
);
// tslint:enable:max-line-length
}
}

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

@ -74,7 +74,7 @@ export default class Home extends React.Component<any, IHomeState> {
this.updateConfiguration = this.updateConfiguration.bind(this);
}
updateConfiguration(state) {
updateConfiguration(state: {templates: IDashboardConfig[], template: IDashboardConfig, creationState: string}) {
this.setState({
templates: state.templates || [],
template: state.template,
@ -82,7 +82,7 @@ export default class Home extends React.Component<any, IHomeState> {
});
}
updateSetup(state) {
updateSetup(state: IHomeState) {
this.setState(state);
// Setup hasn't been configured yet

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

@ -51,6 +51,7 @@ export default class Navbar extends React.Component<any, any> {
try { pathname = window.location.pathname; } catch (e) { }
let navigationItems = [];
let toolbarTitle = null;
(dashboards || []).forEach((dashboard, index) => {
let name = dashboard.name || null;
@ -58,6 +59,12 @@ export default class Navbar extends React.Component<any, any> {
let active = pathname === url;
if (!title && active && name) {
title = name;
toolbarTitle = !dashboard.logo ? name : (
<span>
<span className="title-logo"><img src={dashboard.logo} /></span>
<span>{name}</span>
</span>
);
}
navigationItems.push(
@ -156,7 +163,7 @@ export default class Navbar extends React.Component<any, any> {
mobileDrawerType={drawerType}
tabletDrawerType={drawerType}
desktopDrawerType={drawerType}
toolbarTitle={title}
toolbarTitle={toolbarTitle}
toolbarActions={toolbarActions}
>
{children}

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

@ -51,6 +51,7 @@ export default class Setup extends React.Component<any, ISetupState> {
}
validateEmail(email: string): boolean {
// tslint:disable-next-line:max-line-length
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
@ -65,7 +66,7 @@ export default class Setup extends React.Component<any, ISetupState> {
this.setState({ admins });
e.target.value = '';
} else {
this.setState({ validEmail: false })
this.setState({ validEmail: false });
}
return false;
}
@ -75,7 +76,7 @@ export default class Setup extends React.Component<any, ISetupState> {
}
onSave () {
SetupActions.save({
var setupConfig = {
admins: this.state.admins,
stage: this.state.stage,
enableAuthentication: this.state.enableAuthentication,
@ -83,9 +84,8 @@ export default class Setup extends React.Component<any, ISetupState> {
redirectUrl: this.state.redirectUrl,
clientID: this.state.clientID,
clientSecret: this.state.clientSecret
}, () => {
window.location.replace('/');
});
};
SetupActions.save(setupConfig, () => { window.location.replace('/'); });
}
onCancel () {
@ -102,15 +102,15 @@ export default class Setup extends React.Component<any, ISetupState> {
}
}
onSwitchAuthenticationEnables (checked) {
onSwitchAuthenticationEnables(checked: boolean) {
this.setState({ enableAuthentication: checked });
};
onSwitchAllowHttp (checked) {
onSwitchAllowHttp(checked: boolean) {
this.setState({ allowHttp: checked });
};
onFieldChange (value: string, e: any) {
onFieldChange(value: string, e: any) {
let state = {};
state[e.target.id] = value;
this.setState(state);
@ -138,6 +138,7 @@ export default class Setup extends React.Component<any, ISetupState> {
/>
));
// tslint:disable:max-line-length
return (
<div style={{ width: '100%' }}>
<Switch
@ -149,13 +150,13 @@ export default class Setup extends React.Component<any, ISetupState> {
/>
<InfoDrawer
width={300}
title='Authentication'
buttonIcon='help'
buttonTooltip='Click here to learn more about authentications'
title="Authentication"
buttonIcon="help"
buttonTooltip="Click here to learn more about authentications"
>
<div>
Follow the instructions
in <a href='https://auth0.com/docs/connections/enterprise/azure-active-directory' target='_blank'>this link</a> to
in <a href="https://auth0.com/docs/connections/enterprise/azure-active-directory" target="_blank">this link</a> to
get <b>Client ID</b> and <b>Client Secret</b>
<hr/>
Once you set up authentication, the first user you will log in with, will become the administrator.
@ -166,7 +167,7 @@ export default class Setup extends React.Component<any, ISetupState> {
<br />
{
enableAuthentication &&
enableAuthentication && (
<div>
<Switch
id="allowHttp"
@ -215,11 +216,12 @@ export default class Setup extends React.Component<any, ISetupState> {
defaultValue={clientSecret}
onChange={this.onFieldChange}
/>
</div>
</div>)
}
<Button flat primary label="Save &amp; Apply" onClick={this.onSave}>save</Button>
<Button flat primary label="Cancel" onClick={this.onCancel}>undo</Button>
</div>
);
// tslint:enable:max-line-length
}
}

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

@ -16,7 +16,7 @@ interface ISpinnerState extends ISpinnerStoreState {
export default class Spinner extends React.Component<any, ISpinnerState> {
constructor(props) {
constructor(props: any) {
super(props);
this.state = SpinnerStore.getState();
@ -25,28 +25,28 @@ export default class Spinner extends React.Component<any, ISpinnerState> {
this._429ApplicationInsights = this._429ApplicationInsights.bind(this);
var self = this;
var open_original = XMLHttpRequest.prototype.open;
var send_original = XMLHttpRequest.prototype.send;
var openOriginal = XMLHttpRequest.prototype.open;
var sendOriginal = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url, async, unk1, unk2) {
XMLHttpRequest.prototype.open = function(method: string, url: string, async?: boolean, _?: string, __?: string) {
SpinnerActions.startRequestLoading();
open_original.apply(this, arguments);
openOriginal.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(data) {
XMLHttpRequest.prototype.send = function(data: any) {
let _xhr: XMLHttpRequest = this;
_xhr.onreadystatechange = (response) => {
// readyState === 4: means the response is complete
if(_xhr.readyState === 4) {
if (_xhr.readyState === 4) {
SpinnerActions.endRequestLoading();
if (_xhr.status === 429) {
self._429ApplicationInsights();
}
}
}
send_original.apply(_xhr, arguments);
};
sendOriginal.apply(_xhr, arguments);
};
// Todo: Add timeout to requests - if no reply received, turn spinner off
@ -57,11 +57,11 @@ export default class Spinner extends React.Component<any, ISpinnerState> {
}
_429ApplicationInsights() {
let toast : IToast = { text: 'You have reached the maximum number of Application Insights requests.' };
let toast: IToast = { text: 'You have reached the maximum number of Application Insights requests.' };
ToastActions.addToast(toast);
}
onChange(state) {
onChange(state: ISpinnerState) {
this.setState(state);
}
@ -71,8 +71,8 @@ export default class Spinner extends React.Component<any, ISpinnerState> {
return (
<div>
{ refreshing && <CircularProgress key="progress" id="contentLoadingProgress" /> }
{refreshing && <CircularProgress key="progress" id="contentLoadingProgress" />}
</div>
)
);
}
}

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

@ -8,7 +8,7 @@ interface ISpinnerActions {
}
class SpinnerActions extends AbstractActions /*implements ISpinnerActions*/ {
constructor(alt:AltJS.Alt) {
constructor(alt: AltJS.Alt) {
super(alt);
this.generateActions(

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

@ -3,11 +3,11 @@ import alt, { AbstractStoreModel } from '../../alt';
import spinnerActions from './SpinnerActions';
export interface ISpinnerStoreState {
pageLoading?: number
requestLoading?: number
mounted: boolean
currentBreakpoint: string
layouts: object
pageLoading?: number;
requestLoading?: number;
mounted: boolean;
currentBreakpoint: string;
layouts: object;
}
class SpinnerStore extends AbstractStoreModel<ISpinnerStoreState> implements ISpinnerStoreState {
@ -52,6 +52,6 @@ class SpinnerStore extends AbstractStoreModel<ISpinnerStoreState> implements ISp
}
}
const spinnerStore = alt.createStore<ISpinnerStoreState>(SpinnerStore, "SpinnerStore");
const spinnerStore = alt.createStore<ISpinnerStoreState>(SpinnerStore, 'SpinnerStore');
export default spinnerStore;

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

@ -16,10 +16,6 @@ export default class Toast extends React.Component<any, IToastStoreState> {
this.removeToast = this.removeToast.bind(this);
}
private removeToast() {
ToastActions.removeToast();
}
onChange(state: any) {
this.setState(state);
}
@ -38,4 +34,8 @@ export default class Toast extends React.Component<any, IToastStoreState> {
/>
);
}
private removeToast() {
ToastActions.removeToast();
}
}

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

@ -3,15 +3,15 @@ import alt, { AbstractStoreModel } from '../../alt';
import toastActions from './ToastActions';
export interface IToast {
text: string,
action?: any
text: string;
action?: any;
}
export interface IToastStoreState {
toasts: IToast[],
queued: Array<IToast>,
autohideTimeout: number,
autohide: boolean
toasts: IToast[];
queued: Array<IToast>;
autohideTimeout: number;
autohide: boolean;
}
const MIN_TIMEOUT_MS = 3000;
@ -22,7 +22,6 @@ class ToastStore extends AbstractStoreModel<IToastStoreState> implements IToastS
queued: Array<IToast>;
autohideTimeout: number;
autohide: boolean;
constructor() {
super();
@ -39,7 +38,7 @@ class ToastStore extends AbstractStoreModel<IToastStoreState> implements IToastS
}
addToast(toast: IToast): void {
if ( this.toasts.findIndex(x => x.text === toast.text) > -1 || this.queued.findIndex(x => x.text === toast.text) > -1 ) {
if (this.toastExists(toast)) {
return; // ignore dups
}
if (this.toasts.length === 0) {
@ -50,12 +49,6 @@ class ToastStore extends AbstractStoreModel<IToastStoreState> implements IToastS
this.updateSnackbarAttributes(toast);
}
private updateSnackbarAttributes(toast: IToast): void {
const words = toast.text.split(' ').length;
this.autohideTimeout = Math.max(MIN_TIMEOUT_MS, (words/AVG_WORDS_PER_SEC)*1000);
this.autohide = !toast.action;
}
removeToast(): void {
if (this.queued.length > 0) {
this.toasts = this.queued.splice(0, 1);
@ -64,8 +57,19 @@ class ToastStore extends AbstractStoreModel<IToastStoreState> implements IToastS
this.toasts = toasts;
}
}
private toastExists(toast: IToast): boolean {
return this.toasts.findIndex(x => x.text === toast.text) > -1
|| this.queued.findIndex(x => x.text === toast.text) > -1;
}
private updateSnackbarAttributes(toast: IToast): void {
const words = toast.text.split(' ').length;
this.autohideTimeout = Math.max(MIN_TIMEOUT_MS, (words / AVG_WORDS_PER_SEC) * 1000);
this.autohide = !toast.action;
}
}
const toastStore = alt.createStore<IToastStoreState>(ToastStore, "ToastStore");
const toastStore = alt.createStore<IToastStoreState>(ToastStore, 'ToastStore');
export default toastStore;

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

@ -5,10 +5,19 @@ import injectTooltip from 'react-md/lib/Tooltips';
// Material icons shouldn't have any other children other than the child string and
// it gets converted into a span if the tooltip is added, so we add a container
// around the two.
const TooltipFontIcon = injectTooltip(({ children, iconClassName, className, tooltip, forceIconFontSize, forceIconSize, style, iconStyle, ...props }) => (
const TooltipFontIcon = injectTooltip(({
children, iconClassName, className, tooltip, forceIconFontSize, forceIconSize, style, iconStyle, ...props }) => (
<div {...props} style={style} className={(className || '') + ' inline-rel-container'}>
{tooltip}
<FontIcon style={iconStyle} iconClassName={iconClassName} forceFontSize={forceIconFontSize} forceSize={forceIconSize}>{children}</FontIcon>
<FontIcon
style={iconStyle}
iconClassName={iconClassName}
forceFontSize={forceIconFontSize}
forceSize={forceIconSize}
>
{children}
</FontIcon>
</div>
));

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

@ -49,13 +49,21 @@ export default class BarData extends GenericComponent<IBarProps, IBarState> {
return null;
}
if (!values || !values.length) {
return (
<Card title={title} subtitle={subtitle}>
<div style={{ padding: 20 }}>No data is available</div>
</Card>
);
}
var barElements = [];
if (values && values.length && bars) {
barElements = bars.map((bar, idx) => {
return (
<Bar
key={idx}
stackId='1'
stackId="1"
dataKey={bar.name || bar}
fill={bar.color || ThemeColors[idx]}
onClick={this.handleClick}

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

@ -4,10 +4,10 @@ import Checkbox from 'react-md/lib/SelectionControls/Checkbox';
const style = {
checkbox: {
float: "left",
paddingTop: "24px"
float: 'left',
paddingTop: '24px'
}
}
};
export default class CheckboxFilter extends GenericComponent<any, any> {
@ -16,13 +16,13 @@ export default class CheckboxFilter extends GenericComponent<any, any> {
selectedValues: []
};
constructor(props) {
constructor(props: any) {
super(props);
this.onChange = this.onChange.bind(this);
}
onChange(newValue, checked, event) {
onChange(newValue: string, checked: boolean, event: React.ChangeEvent<string>) {
var { selectedValues } = this.state;
let newSelectedValues = selectedValues.slice(0);
@ -32,7 +32,7 @@ export default class CheckboxFilter extends GenericComponent<any, any> {
} else if (idx > -1 && !checked) {
newSelectedValues.splice(idx, 1);
} else {
console.warn("Unexpected checked filter state:", newValue, checked);
console.warn('Unexpected checked filter state:', newValue, checked);
}
this.trigger('onChange', newSelectedValues);
@ -44,15 +44,18 @@ export default class CheckboxFilter extends GenericComponent<any, any> {
values = values || [];
let checkboxes = values.map((value, idx) => {
return (<Checkbox
key={idx}
id={idx}
name={value}
label={value}
onChange={this.onChange.bind(null, value)}
style={style.checkbox}
checked={selectedValues.find((x) => x === value) !== undefined} />);
})
return (
<Checkbox
key={idx}
id={idx}
name={value}
label={value}
onChange={this.onChange.bind(null, value)}
style={style.checkbox}
checked={selectedValues.find((x) => x === value) !== undefined}
/>
);
});
return (
<div id="filters">

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

@ -48,7 +48,7 @@ export default class Detail extends GenericComponent<IDetailProps, IDetailState>
const header = cols[ci].header;
const field = cols[ci].field;
const data = value[field];
const key = ri + "-" + ci;
const key = ri + '-' + ci;
const content = this.renderData(data);
return (
@ -56,14 +56,14 @@ export default class Detail extends GenericComponent<IDetailProps, IDetailState>
<h6>{header}</h6>
<div className="content">{content}</div>
</li>
)
);
});
return (
<ul key={ri} className="details">
{items}
</ul>
)
);
});
return (
@ -74,12 +74,12 @@ export default class Detail extends GenericComponent<IDetailProps, IDetailState>
}
private renderData(data: any): any {
if (data && data.length > 1 && data.substr(0, 1) === '[' && data.substr(-1) == ']') {
if (data && data.length > 1 && data.substr(0, 1) === '[' && data.substr(-1) === ']') {
const obj = JSON.parse(data);
if (Array.isArray(obj)) {
return this.renderArray(obj);
}
} else if (data && data.length > 1 && data.substr(0, 1) === '{' && data.substr(-1) == '}') {
} else if (data && data.length > 1 && data.substr(0, 1) === '{' && data.substr(-1) === '}') {
const obj = JSON.parse(data);
if (typeof obj === 'object') {
return this.renderObject(obj);
@ -94,7 +94,7 @@ export default class Detail extends GenericComponent<IDetailProps, IDetailState>
<ul>
{contents}
</ul>
)
);
}
private renderObject(data: any): any {

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

@ -1,6 +1,6 @@
import * as React from 'react';
import { DataSourceConnector, IDataSourceDictionary } from '../../../data-sources'
import { DataSourceConnector, IDataSourceDictionary } from '../../../data-sources';
import ElementConnector from '../../ElementConnector';
import DialogsActions from './DialogsActions';
@ -33,7 +33,7 @@ export default class Dialog extends React.PureComponent<IDialogProps, IDialogSta
layouts = {};
constructor(props) {
constructor(props: IDialogProps) {
super(props);
this.state = DialogsStore.getState();
@ -81,23 +81,25 @@ export default class Dialog extends React.PureComponent<IDialogProps, IDialogSta
currentBreakpoint: breakpoint,
layouts: layouts
});
};
}
onChange(state) {
onChange(state: IDialogState) {
var { dialogId, dialogArgs } = state;
this.setState({ dialogId, dialogArgs });
}
closeDialog = () => {
DialogsActions.closeDialog();
};
}
render() {
const { dialogData, dashboard } = this.props;
const { id } = dialogData;
const { dialogId, dialogArgs } = this.state;
let { title } = dialogArgs || { title : '' };
if (title === undefined) title = '';
let { title } = dialogArgs || { title: '' };
if (title === undefined) {
title = '';
}
var visible = id === dialogId;
if (!visible) {
@ -114,10 +116,10 @@ export default class Dialog extends React.PureComponent<IDialogProps, IDialogSta
}
// Creating visual elements
var elements = ElementConnector.loadElementsFromDashboard(dialogData, layout)
var elements = ElementConnector.loadElementsFromDashboard(dialogData, layout);
let grid = {
className: "layout",
className: 'layout',
rowHeight: dashboard.config.layout.rowHeight || 30,
cols: dashboard.config.layout.cols,
breakpoints: dashboard.config.layout.breakpoints
@ -134,19 +136,20 @@ export default class Dialog extends React.PureComponent<IDialogProps, IDialogSta
contentStyle={{ padding: '0', maxHeight: 'calc(100vh - 148px)' }}
>
<ResponsiveReactGridLayout
{ ...grid }
{...grid}
isDraggable={false}
isResizable={false}
layouts={ this.layouts }
layouts={this.layouts}
onBreakpointChange={this.onBreakpointChange}
// WidthProvider option
measureBeforeMount={false}
// I like to have it animate on mount. If you don't, delete `useCSSTransforms` (it's default `true`)
// and set `measureBeforeMount={true}`.
useCSSTransforms={this.state.mounted}>
{ elements }
useCSSTransforms={this.state.mounted}
>
{elements}
</ResponsiveReactGridLayout>
</MDDialog>
);

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

@ -1,16 +1,16 @@
import alt, { AbstractActions } from '../../../alt';
interface IDialogsActions {
openDialog(dialogName: string, args: { [id: string] : Object }): any;
openDialog(dialogName: string, args: { [id: string]: Object }): any;
closeDialog(): any;
}
class DialogsActions extends AbstractActions implements IDialogsActions {
constructor(alt:AltJS.Alt) {
constructor(alt: AltJS.Alt) {
super(alt);
}
openDialog(dialogName: string, args: { [id: string] : Object }) {
openDialog(dialogName: string, args: { [id: string]: Object }) {
return { dialogName, args };
}

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

@ -3,9 +3,9 @@ import alt, { AbstractStoreModel } from '../../../alt';
import dialogsActions from './DialogsActions';
interface IDialogsStoreState {
dialogsStack: { dialogName: string, args: any }[],
dialogId: string,
dialogArgs: any
dialogsStack: { dialogName: string, args: any }[];
dialogId: string;
dialogArgs: any;
}
class DialogsStore extends AbstractStoreModel<IDialogsStoreState> implements IDialogsStoreState {
@ -43,6 +43,6 @@ class DialogsStore extends AbstractStoreModel<IDialogsStoreState> implements IDi
}
}
const dialogsStore = alt.createStore<IDialogsStoreState>(DialogsStore, "DialogsStore");
const dialogsStore = alt.createStore<IDialogsStoreState>(DialogsStore, 'DialogsStore');
export default dialogsStore;

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

@ -2,16 +2,13 @@ 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 } from 'recharts';
import { Tooltip, ResponsiveContainer, Legend, defs } from 'recharts';
import Card from '../CardMap';
import { render } from 'react-dom';
import CircularProgress from 'react-md/lib/Progress/CircularProgress';
import Card from '../Card';
import * as L from 'leaflet';
import { Map, Marker, Popup, TileLayer } from 'react-leaflet';
import DivIcon from 'react-leaflet-div-icon';
import MarkerClusterGroup from 'react-leaflet-markercluster';
import { EsriProvider } from 'leaflet-geosearch';
import CircularProgress from 'react-md/lib/Progress/CircularProgress';
const styles = {
map: {
@ -41,6 +38,9 @@ const provider = new EsriProvider(); // does the search from address to lng and
interface IMapDataProps extends IGenericProps {
mapProps: any;
props: {
searchLocations: boolean;
};
};
interface IMapDataState extends IGenericState {
@ -51,8 +51,8 @@ interface IMapDataState extends IGenericState {
export default class MapData extends GenericComponent<IMapDataProps, IMapDataState> {
static defaultProps = {
center: [34.704929, -81.210251],
zoom: 2,
center: [14.704929, -25.210251],
zoom: 1.4,
maxZoom: 8,
};
@ -69,31 +69,43 @@ export default class MapData extends GenericComponent<IMapDataProps, IMapDataSta
L.Icon.Default.imagePath = 'https://unpkg.com/leaflet@1.0.2/dist/images/';
}
compareMarkers(markers1: any[], markers2: any[]): boolean {
if (markers1 == markers2) { return true; } /* tslint:disable-line */
if (!markers1 || !markers2) { return false; }
if (markers1.length !== markers2.length) { return false; }
return _.isEqualWith(markers1, markers2, (a, b) => a.lat === b.lat && a.lng === b.lng);
}
shouldComponentUpdate(nextProps: any, nextState: any) {
if (!_.isEqual(this.state.locations, nextState.locations)) {
if (this.compareMarkers(this.state.locations, nextState.locations) &&
this.compareMarkers(this.state.markers, nextState.markers)) {
return false;
}
return true;
}
componentDidUpdate() {
const { searchLocations } = this.props.props;
const { locations } = this.state;
if (!locations || !locations.length) { return; }
if (!searchLocations) {
this.setState({ markers: locations });
return;
}
let promises = [];
let markers = [];
locations.forEach(loc => {
const { location, location_count } = loc;
if (location_count === 0) {
return;
}
let { location, popup } = loc;
let promise = provider.search({ query: location });
promises.push(promise);
promise.then(results => {
const popup = location + ' ' + location_count;
markers.push({ lat: results[0].y, lng: results[0].x, popup: popup });
let markupPopup = (popup && L.popup().setContent(popup)) || null;
markers.push({ lat: results[0].y, lng: results[0].x, popup: markupPopup });
});
});
@ -112,8 +124,7 @@ export default class MapData extends GenericComponent<IMapDataProps, IMapDataSta
render() {
const { markers } = this.state;
const { title, subtitle, props } = this.props;
const { mapProps } = props;
const { title, subtitle, props, mapProps } = this.props;
if (!markers) {
return null;
@ -146,6 +157,9 @@ export default class MapData extends GenericComponent<IMapDataProps, IMapDataSta
/>
<MarkerClusterGroup
markers={markers}
options={{
maxClusterRadius: 10,
}}
wrapperOptions={{ enableDefaultStyle: true }}
/>
</Map>

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

@ -75,15 +75,15 @@ export default class PieData extends GenericComponent<IPieProps, IPieState> {
c.ey = c.my;
c.textAnchor = 'start';
var text = compact
? [<text key={0} x={cx} y={cy} dy={-15} textAnchor="middle" fill={fill} style={{ fontWeight: 500 }}>{name}</text>,
<text key={1} x={cx} y={cy} dy={3} textAnchor="middle" fill={fill}>{`${value} ${entityType}`}</text>,
<text key={2} x={cx} y={cy} dy={25} textAnchor="middle" fill="#999">{`(${(percent * 100).toFixed(2)}%)`}</text>]
: [<text key={3} x={cx} y={cy} dy={8} textAnchor="middle" fill={fill}>{name}</text>];
return (
<g>
{compact && [
<text key={0} x={cx} y={cy} dy={-15} textAnchor="middle" fill={fill} style={{ fontWeight: 500 }}>{name}</text>,
<text key={1} x={cx} y={cy} dy={3} textAnchor="middle" fill={fill}>{`${value} ${entityType}`}</text>,
<text key={2} x={cx} y={cy} dy={25} textAnchor="middle" fill="#999">{`(${(percent * 100).toFixed(2)}%)`}</text>
] || [
<text key={3} x={cx} y={cy} dy={8} textAnchor="middle" fill={fill}>{name}</text>,
]}
{text}
<Sector
cx={cx}
cy={cy}

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

@ -49,12 +49,12 @@ export default class RadarChartCard extends GenericComponent<IRadarProps, IRadar
const domain = 100;
const data05 = [
{ subject: 'Math', "NFL": 120, "NBA": 110, fullMark: domain },
{ subject: 'Chinese', "NFL": 98, "NBA": 30, fullMark: domain },
{ subject: 'English', "NFL": 86, "NBA": 130, fullMark: domain },
{ subject: 'Geography', "NFL": 110, "NBA": 95, fullMark: domain },
{ subject: 'Physics', "NFL": 102, "NBA": 90, fullMark: domain },
{ subject: 'History', "NFL": 65, "NBA": 85, fullMark: domain },
{ subject: 'Math', 'NFL': 120, 'NBA': 110, fullMark: domain },
{ subject: 'Chinese', 'NFL': 98, 'NBA': 30, fullMark: domain },
{ subject: 'English', 'NFL': 86, 'NBA': 130, fullMark: domain },
{ subject: 'Geography', 'NFL': 110, 'NBA': 95, fullMark: domain },
{ subject: 'Physics', 'NFL': 102, 'NBA': 90, fullMark: domain },
{ subject: 'History', 'NFL': 65, 'NBA': 85, fullMark: domain },
];
return (

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

@ -65,8 +65,8 @@ export default class RadialBarChartCard extends GenericComponent<IRadarProps, IR
outerRadius="80%"
data={data}
>
<RadialBar startAngle={90} endAngle={-270} minAngle={15} label background clockWise={true} dataKey='uv' />
<Legend iconSize={10} width={120} height={140} layout='vertical' verticalAlign='middle' align="right" />
<RadialBar startAngle={90} endAngle={-270} minAngle={15} label background clockWise={true} dataKey="uv" />
<Legend iconSize={10} width={120} height={140} layout="vertical" verticalAlign="middle" align="right" />
<Tooltip />
</RadialBarChart>
</ResponsiveContainer>

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

@ -9,9 +9,9 @@ import utils from '../../utils';
const styles = {
chevron: {
float: "none",
float: 'none',
padding: 0,
verticalAlign: "middle"
verticalAlign: 'middle'
}
};
@ -21,7 +21,7 @@ interface IScorecardProps extends IGenericProps {
colorPosition?: 'bottom' | 'left';
subheading?: string;
onClick?: string;
}
};
}
export default class Scorecard extends GenericComponent<IScorecardProps, any> {
@ -44,11 +44,6 @@ export default class Scorecard extends GenericComponent<IScorecardProps, any> {
let { title, props, actions } = this.props;
let { subheading, colorPosition, scorecardWidth, onClick } = props;
let style = {};
if (scorecardWidth) {
style['width'] = scorecardWidth;
}
if (_.has(this.state, 'values')) {
// In case the user defined a "values" parameter
@ -85,52 +80,12 @@ export default class Scorecard extends GenericComponent<IScorecardProps, any> {
values = Object.keys(dynamicCards).map(key => dynamicCards[key]);
}
values = values || [];
var cards = values.map((value, idx) => {
let colorStyle = {};
let cardstyle = _.extend({}, style);
let color = value.color || '';
let icon = value.icon;
let iconStyle = icon && { color };
let onClick = value.onClick;
let chevronStyle = _.extend({}, styles.chevron);
chevronStyle['color'] = color;
if (!icon || colorPosition) {
if (!colorPosition || colorPosition === 'bottom') { colorStyle['borderColor'] = color; }
if (colorPosition === 'left') { cardstyle['borderColor'] = color; }
}
const drillDownLink = onClick ?
<div className="md-subheading-2" style={{ color: color }}>{value.heading} <FontIcon style={chevronStyle}>chevron_right</FontIcon></div>
: <div className="md-subheading-2">{value.heading}</div>;
let cardClassName = 'scorecard ' + (onClick ? 'clickable-card ' : '') + (colorPosition ? 'color-' + colorPosition : '');
return (
<div key={idx} className={cardClassName} style={cardstyle} onClick={this.handleClick.bind(this, value)}>
{
icon && <FontIcon className={className} style={iconStyle}>{icon}</FontIcon>
}
<div className="md-headline">{this.shortFormatter(value.value)}</div>
{drillDownLink}
{
(value.subvalue || value.subheading) &&
(
<div className="scorecard-subheading" style={colorStyle}>
<b>{this.shortFormatter(value.subvalue)}</b>
{value.subheading}
</div>
)
}
</div>
)
});
var cards = (values || []).map((val, idx) =>
this.valueToCard(val, idx, className, colorPosition, scorecardWidth));
return (
<Card>
<Media className='md-card-scorecard'>
<Media className="md-card-scorecard">
<div className="md-grid--no-spacing">
{cards}
</div>
@ -139,7 +94,7 @@ export default class Scorecard extends GenericComponent<IScorecardProps, any> {
);
}
handleClick(value, proxy) {
handleClick(value: {onClick: string}, proxy: any) {
if (value && value.onClick && _.isEmpty(this.props.actions)) {
return;
}
@ -149,4 +104,52 @@ export default class Scorecard extends GenericComponent<IScorecardProps, any> {
var args = { ...value };
this.trigger(value.onClick, args);
}
private valueToCard(value: any, idx: number, className: string, colorPosition: string, scorecardWidth: number) {
let style = {};
if (scorecardWidth) {
style['width'] = scorecardWidth;
}
let colorStyle = {};
let cardstyle = _.extend({}, style);
let color = value.color || '';
let icon = value.icon;
let iconStyle = icon && { color };
let onClick = value.onClick;
let chevronStyle = _.extend({}, styles.chevron);
chevronStyle['color'] = color;
if (!icon || colorPosition) {
if (!colorPosition || colorPosition === 'bottom') { colorStyle['borderColor'] = color; }
if (colorPosition === 'left') { cardstyle['borderColor'] = color; }
}
const drillDownLink = onClick ? (
<div className="md-subheading-2" style={{ color: color }}>
{value.heading}
<FontIcon style={chevronStyle}>chevron_right</FontIcon>
</div>)
: <div className="md-subheading-2">{value.heading}</div>;
let cardClassName = `scorecard${onClick ? ' clickable-card' : ''}${colorPosition ? ` color-${colorPosition}` : ''}`;
return (
<div key={idx} className={cardClassName} style={cardstyle} onClick={this.handleClick.bind(this, value)}>
{
icon && <FontIcon className={className} style={iconStyle}>{icon}</FontIcon>}
<div className="md-headline">{this.shortFormatter(value.value)}</div>
{drillDownLink}
{
(value.subvalue || value.subheading) &&
(
<div className="scorecard-subheading" style={colorStyle}>
<b>{this.shortFormatter(value.subvalue)}</b>
{value.subheading}
</div>
)
}
</div>
);
}
}

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

@ -72,8 +72,15 @@ export default class SimpleRadialBarChartCard extends GenericComponent<IRadarPro
barSize={10}
data={data}
>
<RadialBar minAngle={15} label background clockWise={true} dataKey='uv' />
<Legend iconSize={10} width={120} height={140} layout='vertical' verticalAlign='middle' wrapperStyle={style} />
<RadialBar minAngle={15} label background clockWise={true} dataKey="uv" />
<Legend
iconSize={10}
width={120}
height={140}
layout="vertical"
verticalAlign="middle"
wrapperStyle={style}
/>
</RadialBarChart>
</ResponsiveContainer>
</Card>

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

@ -135,7 +135,7 @@ export default class SplitPanel extends GenericComponent<ISplitViewProps, ISplit
return (
<Card>
<div style={style.lhs} className='split-view'>
<div style={style.lhs} className="split-view">
<List>
{listItems}
</List>

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

@ -21,6 +21,7 @@ export interface ITableColumnProps {
width?: string | number;
type?: ColType;
click?: string;
color?: string;
}
export interface ITableProps extends IGenericProps {
@ -96,14 +97,16 @@ export default class Table extends GenericComponent<ITableProps, ITableState> {
let pageValues = values.slice(rowIndex, rowIndex + rowsPerPage) || [];
let renderColumn = (col: ITableColumnProps, value: any): JSX.Element => {
let style = { color: col.color ? value[col.color] : null };
switch (col.type) {
case 'icon':
return <FontIcon>{col.value || value[col.field]}</FontIcon>;
return <FontIcon style={style}>{col.value || value[col.field]}</FontIcon>;
case 'button':
return (
<Button
style={style}
icon={true}
onClick={this.onButtonClick.bind(this, col, value)}
>
@ -112,26 +115,27 @@ export default class Table extends GenericComponent<ITableProps, ITableState> {
);
case 'time':
return <span>{moment(value[col.field]).format('MMM-DD HH:mm:ss')}</span>;
return <span style={style}>{moment(value[col.field]).format('MMM-DD HH:mm:ss')}</span>;
case 'number':
return <span>{utils.kmNumber(value[col.field])}</span>
return <span style={style}>{utils.kmNumber(value[col.field])}</span>;
case 'ago':
return <span>{utils.ago(value[col.field])}</span>
return <span style={style}>{utils.ago(value[col.field])}</span>;
default:
if (col.secondaryField !== undefined)
if (col.secondaryField !== undefined) {
return (
<div className="table">
<div className="table" style={style}>
<span className="primary">{value[col.field]}</span>
<span className="secondary">{value[col.secondaryField]}</span>
</div>
);
else
return <span>{value[col.field]}</span>;
} else {
return <span style={style}>{value[col.field]}</span>;
}
}
}
};
const rows = pageValues.map((value, ri) => (
<TableRow

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

@ -36,8 +36,7 @@ export class DataSourceConnector {
}
// Dynamically load the plugin from the plugins directory
var pluginPath = './plugins/' + config.type;
var PluginClass = require(pluginPath);
var PluginClass = require('./plugins/' + config.type);
var plugin: any = new PluginClass.default(config, connections);
// Creating actions class
@ -67,45 +66,6 @@ export class DataSourceConnector {
DataSourceConnector.initializeDataSources();
}
private static connectDataSource(sourceDS: IDataSource) {
// Connect sources and dependencies
sourceDS.store.listen((state) => {
Object.keys(this.dataSources).forEach(checkDSId => {
var checkDS = this.dataSources[checkDSId];
var dependencies = checkDS.plugin.getDependencies() || {};
let connected = _.find(_.keys(dependencies), dependencyKey => {
let dependencyValue = dependencies[dependencyKey] || '';
return (dependencyValue === sourceDS.id || dependencyValue.startsWith(sourceDS.id + ':'));
})
if (connected) {
// Todo: add check that all dependencies are met
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 initialize methods
Object.keys(this.dataSources).forEach(sourceDSId => {
@ -174,7 +134,7 @@ export class DataSourceConnector {
});
if (updateVisibility) {
(<any>VisibilityActions.setFlags).defer(visibilityFlags);
(VisibilityActions.setFlags as any).defer(visibilityFlags);
}
return result;
@ -219,10 +179,49 @@ export class DataSourceConnector {
return this.dataSources[name];
}
private static createActionClass(plugin: IDataSourcePlugin) : any {
private static connectDataSource(sourceDS: IDataSource) {
// Connect sources and dependencies
sourceDS.store.listen((state) => {
Object.keys(this.dataSources).forEach(checkDSId => {
var checkDS = this.dataSources[checkDSId];
var dependencies = checkDS.plugin.getDependencies() || {};
let connected = _.find(_.keys(dependencies), dependencyKey => {
let dependencyValue = dependencies[dependencyKey] || '';
return (dependencyValue === sourceDS.id || dependencyValue.startsWith(sourceDS.id + ':'));
});
if (connected) {
// Todo: add check that all dependencies are met
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) {
(VisibilityActions.setFlags as any).defer(updatedFlags);
}
});
}
private static createActionClass(plugin: IDataSourcePlugin): any {
class NewActionClass {
constructor() {}
};
}
plugin.getActions().forEach(action => {

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

@ -8,7 +8,7 @@ interface IConnection {
interface IConnectionProps {
connection: any;
onParamChange: (connectionKey, paramId, paramValue) => void;
onParamChange: (connectionKey: string, paramId: string, paramValue: string) => void;
}
abstract class ConnectionEditor<T1 extends IConnectionProps, T2> extends React.Component<T1, T2> {

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

@ -28,18 +28,19 @@ class AIConnectionEditor extends ConnectionEditor<IConnectionProps, any> {
let { connection } = this.props;
connection = connection || {};
// tslint:disable:max-line-length
return (
<div>
<h2 style={{ float: 'left', padding: 9 }}>Application Insights</h2>
<InfoDrawer
width={300}
title='Authentication'
buttonIcon='help'
buttonTooltip='Click here to learn more about authentications'
title="Authentication"
buttonIcon="help"
buttonTooltip="Click here to learn more about authentications"
>
<div>
Follow the instructions
in <a href='https://dev.int.applicationinsights.io/documentation/Authorization/API-key-and-App-ID' target='_blank'>this link</a> to
in <a href="https://dev.int.applicationinsights.io/documentation/Authorization/API-key-and-App-ID" target="_blank">this link</a> to
get <b>Application ID</b> and <b>Api Key</b>
<hr/>
This setup will creates credential for the dashboard to query telemetry information from Application Insights.
@ -64,6 +65,7 @@ class AIConnectionEditor extends ConnectionEditor<IConnectionProps, any> {
onChange={this.onParamChange}
/>
</div>
)
);
// tslint:enable:max-line-length
}
}

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

@ -0,0 +1,93 @@
import * as React from 'react';
import { IConnection, ConnectionEditor, IConnectionProps } from './Connection';
import InfoDrawer from '../../components/common/InfoDrawer';
import TextField from 'react-md/lib/TextFields';
export default class AzureConnection implements IConnection {
type = 'azure';
params = [ 'servicePrincipalId', 'servicePrincipalKey', 'servicePrincipalDomain', 'subscriptionId' ];
editor = AzureConnectionEditor;
}
class AzureConnectionEditor extends ConnectionEditor<IConnectionProps, any> {
constructor(props: IConnectionProps) {
super(props);
this.onParamChange = this.onParamChange.bind(this);
}
onParamChange(value: string, event: any) {
if (typeof this.props.onParamChange === 'function') {
this.props.onParamChange('azure', event.target.id, value);
}
}
render() {
let { connection } = this.props;
connection = connection || {};
let servicePrincipalUrl =
'https://docs.microsoft.com/en-us/azure/azure-resource-manager/' +
'resource-group-create-service-principal-portal';
return (
<div>
<h2 style={{ float: 'left', padding: 9 }}>Azure Connection</h2>
<InfoDrawer
width={300}
title="Authentication"
buttonIcon="help"
buttonTooltip="Click here to learn more about authentications"
>
<div>
Follow the instructions
in <a href={servicePrincipalUrl} target="_blank">this link</a> to
get <b>Service Principal ID</b> and <b>Service Principal Key</b>
<hr/>
This setup will creates credential for the dashboard to query resources from Azure.
</div>
</InfoDrawer>
<TextField
id="servicePrincipalId"
label={'Service Principal Id'}
defaultValue={connection['servicePrincipalId'] || ''}
lineDirection="center"
placeholder="Fill in Service Principal Id"
className="md-cell md-cell--bottom"
onChange={this.onParamChange}
/>
<TextField
id="servicePrincipalKey"
label={'Service Principal Key'}
defaultValue={connection['servicePrincipalKey'] || ''}
lineDirection="center"
placeholder="Fill in Service Principal Key"
className="md-cell md-cell--bottom"
onChange={this.onParamChange}
/>
<TextField
id="servicePrincipalDomain"
label={'Service Principal Domain'}
defaultValue={connection['servicePrincipalDomain'] || ''}
lineDirection="center"
placeholder="Fill in Service Principal Domain"
className="md-cell md-cell--bottom"
onChange={this.onParamChange}
/>
<TextField
id="subscriptionId"
label={'Subscription Id'}
defaultValue={connection['subscriptionId'] || ''}
lineDirection="center"
placeholder="Fill in Subscription Id"
className="md-cell md-cell--bottom"
onChange={this.onParamChange}
/>
</div>
);
}
}

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

@ -2,9 +2,10 @@ import * as React from 'react';
import ApplicationInsightsConnection from './application-insights';
import CosmosDBConnection from './cosmos-db';
import AzureConnection from './azure';
import { IConnection } from './Connection';
var connectionTypes = [ ApplicationInsightsConnection, CosmosDBConnection];
var connectionTypes = [ ApplicationInsightsConnection, AzureConnection, CosmosDBConnection ];
var connections: IDict<IConnection> = {};
connectionTypes.forEach(connectionType => {

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

@ -1,22 +1,18 @@
//import * as request from 'request';
// import * as request from 'request';
import * as _ from 'lodash';
import {DataSourcePlugin, IDataSourceOptions} from '../DataSourcePlugin';
//import ActionsCommon from './actions-common';
// import ActionsCommon from './actions-common';
import { appInsightsUri } from './common';
declare var process : any;
declare var proces: any;
interface IEventsConfig {
/** @type {string} */
timespan;
timespan: string;
}
interface IEventsParams {
/** @type {string} */
query;
/** @type {(string|object)[]} mappings */
mappings;
query: string;
mappings: (string|object)[];
}
export default class ApplicationInsightsEvents extends DataSourcePlugin<IEventsParams> {
@ -34,12 +30,7 @@ export default class ApplicationInsightsEvents extends DataSourcePlugin<IEventsP
// }
}
/**
* update - called when dependencies are created
* @param {object} dependencies
* @param {function} callback
*/
updateDependencies(dependencies, callback) {
updateDependencies(dependencies: IDictionary, callback: (result: any) => void) {
// var {
// timespan,
@ -75,7 +66,7 @@ export default class ApplicationInsightsEvents extends DataSourcePlugin<IEventsP
// });
}
updateSelectedValues(dependencies, callback) {
updateSelectedValues(dependencies: IDictionary, callback: (result: any) => void) {
}
}

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

@ -43,7 +43,9 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
updateDependencies(dependencies: any) {
let emptyDependency = false;
Object.keys(this._props.dependencies).forEach((key) => {
if(typeof dependencies[key] === 'undefined') emptyDependency = true;
if (typeof dependencies[key] === 'undefined') {
emptyDependency = true;
}
});
// If one of the dependencies is not supplied, do not run the query
@ -96,51 +98,59 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
var url = `${appInsightsUri}/${appId}/query?timespan=${queryTimespan}`;
return (dispatch) => {
request(url, {
method: 'POST',
json: true,
headers: {
'x-api-key': apiKey
},
body: {
query
}
}, (error, json) => {
if (error) {
console.log(error);
return this.failure(error);
}
// Check if result is valid
let tables = this.mapAllTables(json, mappings);
let resultStatus: IQueryStatus[] = tables[tables.length - 1];
if (!resultStatus || !resultStatus.length) {
return dispatch(json);
}
// Map tables to appropriate results
var resultTables = tables.filter((aTable, idx) => {
return idx < resultStatus.length && resultStatus[idx].Kind === 'QueryResult';
});
let returnedResults = {
values: (resultTables.length && resultTables[0]) || 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[aTable].calculated;
if (typeof calc === 'function') {
var additionalValues = calc(returnedResults[aTable], dependencies, prevState) || {};
Object.assign(returnedResults, additionalValues);
request(
url,
{
method: 'POST',
json: true,
headers: {
'x-api-key': apiKey
},
body: {
query
}
},
(error, json) => {
if (error) { return this.failure(error); }
if (json.error) {
return json.error.code === 'PathNotFoundError' ?
this.failure(new Error(
`There was a problem getting results from Application Insights. Make sure the connection string is food.
${JSON.stringify(json)}`)) :
this.failure(json.error);
}
});
return dispatch(returnedResults);
});
// Check if result is valid
let tables = this.mapAllTables(json, mappings);
let resultStatus: IQueryStatus[] = tables[tables.length - 1];
if (!resultStatus || !resultStatus.length) {
return dispatch(json);
}
// Map tables to appropriate results
var resultTables = tables.filter((aTable, idx) => {
return idx < resultStatus.length && resultStatus[idx].Kind === 'QueryResult';
});
let returnedResults = {
values: (resultTables.length && resultTables[0]) || 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[aTable].calculated;
if (typeof calc === 'function') {
var additionalValues = calc(returnedResults[aTable], dependencies, prevState) || {};
Object.assign(returnedResults, additionalValues);
}
});
return dispatch(returnedResults);
}
);
};
}
@ -216,7 +226,6 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
return isForked ? ` (${query}) \n\n` : query;
}
private validateTimespan(props: any) {
if (!props.dependencies.queryTimespan) {
throw new Error('AIAnalyticsEvents requires dependencies: timespan; queryTimespan');

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

@ -0,0 +1,132 @@
import * as request from 'xhr-request';
// var msRestAzure = require('ms-rest-azure');
// var resourceManagement = require("azure-arm-resource");
import { DataSourcePlugin, IOptions } from './DataSourcePlugin';
import AzureConnection from '../connections/azure';
import { DataSourceConnector } from '../DataSourceConnector';
let connectionType = new AzureConnection();
interface IAzureParams {
type?: 'locations' | 'resources';
}
interface IFilterParams {
dependency: string;
queryProperty: string;
}
export default class Azure extends DataSourcePlugin<IAzureParams> {
type = 'Azure';
defaultProperty = 'values';
connectionType = connectionType.type;
/**
* @param options - Options object
* @param connections - List of available connections
*/
constructor(options: IOptions<IAzureParams>, connections: IDict<IStringDictionary>) {
super(options, connections);
this.validateParams(this._props.params);
}
/**
* update - called when dependencies are created
* @param {object} dependencies
* @param {function} callback
*/
updateDependencies(dependencies: any) {
let emptyDependency = false;
Object.keys(this._props.dependencies).forEach((key) => {
if (typeof dependencies[key] === 'undefined') { emptyDependency = true; }
});
// If one of the dependencies is not supplied, do not run the query
if (emptyDependency) {
return (dispatch) => {
return dispatch();
};
}
// Validate connection
let connection = this.getConnection();
let { servicePrincipalId, servicePrincipalKey, servicePrincipalDomain, subscriptionId } = connection;
if (!connection || !servicePrincipalId || !servicePrincipalKey) {
return (dispatch) => {
return dispatch();
};
}
let params = this._props.params || {};
let type: string;
let apiVersion = '2016-09-01';
switch (params.type) {
default:
type = params.type;
break;
}
if (type && type.indexOf('Microsoft.Billing/') >= 0) {
apiVersion = '2017-02-27-preview';
}
return (dispatch) => {
request(
'/azure/query',
{
method: 'POST',
json: true,
body: {
servicePrincipalId, servicePrincipalKey, servicePrincipalDomain, subscriptionId,
options: {
url: `/subscriptions/${subscriptionId}/${type}?api-version=${apiVersion}`
}
}
},
(error, json) => {
if (error) { return this.failure(error); }
return dispatch({ values: json });
}
);
};
}
updateSelectedValues(dependencies: IDictionary, selectedValues: any) {
if (Array.isArray(selectedValues)) {
return Object.assign(dependencies, { 'selectedValues': selectedValues });
} else {
return Object.assign(dependencies, { ... selectedValues });
}
}
private validateParams(params: IAzureParams): void {
return;
// if (params.query) {
// if (params.table || params.queries) {
// throw new Error('AI query should either have { query } or { table, queries } under params.');
// }
// if (typeof params.query !== 'string' && typeof params.query !== 'function') {
// throw new Error('{ query } param should either be a function or a string.');
// }
// }
// if (params.table) {
// if (!params.queries) {
// 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: {...} } }.');
// }
// }
// if (!params.query && !params.table) {
// throw new Error('{ table, queries } should be of types { "string", { query1: {...}, query2: {...} } }.');
// }
}
}

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

@ -1,4 +1,6 @@
import { ToastActions } from '../../components/Toast';
export interface IDataSourceOptions {
dependencies: (string | Object)[];
/** This would be variable storing the results */
@ -116,6 +118,23 @@ export abstract class DataSourcePlugin<T> implements IDataSourcePlugin {
}
failure(error: any): void {
ToastActions.addToast({ text: this.errorToMessage(error) });
return error;
}
private errorToMessage(error: any): string {
if (!(error instanceof Error)) {
if (typeof error === 'object') { return JSON.stringify(error); }
return error;
}
const message = (error as Error).message;
if (message === '[object ProgressEvent]') {
return 'There is a problem connecting to the internet.';
}
return `Error: ${message}`;
}
}

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

@ -38,8 +38,6 @@ export default class Sample extends DataSourcePlugin<ISampleParams> {
return result;
}
updateSelectedValues(dependencies: IDictionary, selectedValues: any) {
if (Array.isArray(selectedValues)) {
return _.extend(dependencies, { 'selectedValues': selectedValues });

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

@ -0,0 +1,7 @@
import ApplicationInsightsQuery from './ApplicationInsights/Query';
import Azure from './Azure';
export default {
ApplicationInsightsQuery,
Azure
};

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

@ -1,4 +1,5 @@
@import '../node_modules/react-md/src/scss/react-md';
@import '../node_modules/leaflet/dist/leaflet.css';
$md-primary-color: $md-cyan-500;
$md-secondary-color: $md-pink-a-200;
@ -21,6 +22,21 @@ h2, .md-subheading-2 {
transform: translate3d(0px, 0px, 0px) scale(1);
}
/**
*/
.md-title--toolbar {
.title-logo {
float: left;
padding-right: 10px;
padding-top: 10px;
img {
max-height: 40px;
max-width: 40px;
}
}
}
/*=============*/
/* Chips Style */
/*=============*/
@ -134,6 +150,7 @@ h2, .md-subheading-2 {
vertical-align: middle;
padding-top: 7px;
padding-bottom: 7px;
padding-right: 16px;
height: 51px;
}
}
@ -197,4 +214,19 @@ h2, .md-subheading-2 {
.widgets {
top: -5px;
}
}
/*======*/
/* Map */
/*======*/
.leaflet-control-attribution {
display: none;
}
.leaflet-popup-content {
b {
font-weight: 600;
}
}

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

@ -22,7 +22,7 @@ export default class Config extends React.Component<any, IDashboardState> {
constructor(props: any) {
super(props);
//ConfigurationsActions.loadConfiguration();
// ConfigurationsActions.loadConfiguration();
}
componentDidMount() {

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

@ -51,9 +51,7 @@ class SetupStore extends AbstractStoreModel<ISetupStoreState> implements ISetupS
this.loaded = true;
this.saveSuccess = true;
setTimeout(() => {
this.saveSuccess = false;
}, 500);
setTimeout(() => { this.saveSuccess = false; }, 500);
}
}

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

@ -1,8 +1,9 @@
import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources';
import { IDataSourceDictionary } from '../../data-sources';
import { setupTests } from '../utils/setup';
import { appInsightsUri } from '../../data-sources/plugins/ApplicationInsights/common';
import { mockRequests } from '../mocks/application-insights/requests';
import dataSourcesMock from '../mocks/application-insights/dataSources';
import { mockRequests } from '../mocks/requests/application-insights';
import dashboardMock from '../mocks/dashboards/application-insights';
describe('Data Source: Application Insights: Query', () => {
@ -11,8 +12,7 @@ describe('Data Source: Application Insights: Query', () => {
beforeAll(() => {
mockRequests();
DataSourceConnector.createDataSources({ dataSources: dataSourcesMock }, {});
dataSources.timespan.action.updateDependencies();
dataSources = setupTests(dashboardMock);
});
it ('Query for 30 months with data rows', function (done) {

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

@ -1,18 +1,18 @@
import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources';
import dataSourceMock from '../mocks/dataSource';
import { IDataSourceDictionary } from '../../data-sources';
import { setupTests } from '../utils/setup';
import dashboardMock from '../mocks/dashboards/constants';
describe('Data Source: Constant', () => {
let dataSources: IDataSourceDictionary = {};
let dataSources: IDataSourceDictionary;
beforeAll(() => {
DataSourceConnector.createDataSources({ dataSources: [ dataSourceMock ]}, {});
beforeAll((done) => {
dataSources = setupTests(dashboardMock, done);
});
it ('Check basic data == 3 rows', () => {
expect(dataSources).toHaveProperty('data');
expect(dataSources).toHaveProperty('data');;
expect(dataSources.data).toHaveProperty('store');
expect(dataSources.data).toHaveProperty('action');
expect(dataSources.data.store).toHaveProperty('state');

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

@ -0,0 +1,26 @@
import { IDataSourceDictionary } from '../../data-sources';
import { setupTests } from '../utils/setup';
import dashboardMock from '../mocks/dashboards/samples';
describe('Data Source: Samples', () => {
let dataSources: IDataSourceDictionary;
beforeAll((done) => {
dataSources = setupTests(dashboardMock, done);
});
it ('Check basic data == 3 rows', () => {
expect(dataSources).toHaveProperty('samples');
expect(dataSources.samples).toHaveProperty('store');
expect(dataSources.samples).toHaveProperty('action');
expect(dataSources.samples.store).toHaveProperty('state', {
values: [
{ id: "value1", count: 60 },
{ id: "value2", count: 10 },
{ id: "value3", count: 30 }
]
});
});
});

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

@ -5,7 +5,7 @@ import * as TestUtils from 'react-addons-test-utils';
import { Dialog, DialogsActions } from '../../components/generic/Dialogs';
import MDDialog from 'react-md/lib/Dialogs';
import dashboard from '../mocks/dashboard';
import dashboard from '../mocks/dashboards/dashboard';
import dialogData from '../mocks/dialog';
describe('Dialog', () => {

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

@ -8,25 +8,30 @@ import { Spinner, SpinnerActions } from '../../components/Spinner';
import Table from '../../components/generic/Table';
import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources';
import dataSourceMock from '../mocks/dataSource';
import tablePropsMock from '../mocks/table';
//import dataSourceMock from '../mocks/dataSource';
import dashboardMock from '../mocks/dashboards/table';
describe('Table', () => {
let dataSources: IDataSourceDictionary = {};
let table;
beforeAll(() => {
beforeAll((done) => {
DataSourceConnector.createDataSources({ dataSources: [ dataSourceMock ]}, {});
DataSourceConnector.createDataSources(dashboardMock, dashboardMock.config.connections);
dataSources = DataSourceConnector.getDataSources();
table = TestUtils.renderIntoDocument(<Table {...tablePropsMock} />);
let {id, dependencies, actions, props, title, subtitle } = dashboardMock.elements[0];
let atts = {id, dependencies, actions, props, title, subtitle };
table = TestUtils.renderIntoDocument(<Table {...(atts as any)} />);
TestUtils.isElementOfType(table, 'div');
setTimeout(done, 100);
})
it('Render inside a Card', () => {
let progress = TestUtils.scryRenderedComponentsWithType(table, Card);
expect(progress.length).toBe(1);
let card = TestUtils.scryRenderedComponentsWithType(table, Card);
expect(card.length).toBe(1);
});
it('Render a Data Table entity', () => {
@ -34,15 +39,17 @@ describe('Table', () => {
expect(progress.length).toBe(1);
});
it('Rows == 19', () => {
it('Rows == 4', () => {
let rows = TestUtils.scryRenderedComponentsWithType(table, TableRow);
expect(rows.length).toBe(19);
expect(rows.length).toBe(4);
});
it('Rows == 25', () => {
dataSources['data'].action.updateDependencies();
it('Rows == 0', () => {
dataSources['samples'].action.updateDependencies({
values: []
});
let rows = TestUtils.scryRenderedComponentsWithType(table, TableRow);
expect(rows.length).toBe(25);
expect(rows.length).toBe(1);
});
afterAll(() => {

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

@ -1,29 +0,0 @@
export default [
{
id: 'timespan',
type: 'Constant',
params: {
values: ['24 hours', '1 week', '1 month'],
selectedValue: '1 month'
},
calculated: (state, dependencies) => {
var queryTimespan = state.selectedValue === '24 hours' ? 'PT24H' : state.selectedValue === '1 week' ? 'P7D' : 'P30D';
var granularity = state.selectedValue === '24 hours' ? '5m' : state.selectedValue === '1 week' ? '1d' : '1d';
return { queryTimespan, granularity };
}
},
{
id: 'events',
type: 'ApplicationInsights/Query',
dependencies: { timespan: 'timespan', queryTimespan: 'timespan:queryTimespan' },
params: {
query: `customEvents`,
mappings: [
{ key: 'name' },
{ key: 'successful', val: (val) => val === 'true' },
{ key: 'event_count', def: 0 }
]
}
}
]

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

@ -0,0 +1,17 @@
import dashboard from './timespan';
dashboard.config.connections["application-insights"] = { appId: '1', apiKey: '1' };
dashboard.dataSources.push({
id: 'events',
type: 'ApplicationInsights/Query',
dependencies: { timespan: 'timespan', queryTimespan: 'timespan:queryTimespan' },
params: {
query: `customEvents`,
mappings: [
{ key: 'name' },
{ key: 'successful', val: (val) => val === 'true' },
{ key: 'event_count', def: 0 }
]
}
});
export default dashboard;

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

@ -1,9 +1,10 @@
var someJsonValues = [
{ id: 1, count: 2 },
{ id: 2, count: 0 }
];
let someJsonValues = [
{ id: 1, count: 2 },
{ id: 2, count: 0 }
];
export default {
import dashboard from './dashboard';
dashboard.dataSources.push({
id: 'data',
type: 'Constant',
params: {
@ -14,4 +15,6 @@ export default {
someJsonValues.push({ id: 3, count: 10 });
return { someJsonValues };
}
}
});
export default dashboard;

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

@ -1,11 +1,11 @@
export default <IDashboardConfig>{
id: 'id',
url: 'url',
icon: 'icon',
name: 'name',
config: {
connections: {},
layout: {
isDraggable: true,
isResizable: true,
rowHeight: 30,
// This turns off compaction so you can place items wherever.
verticalCompact: false,
cols: {lg: 12, md: 10, sm: 6, xs: 4, xxs: 2},
breakpoints: {lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0}
}

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

@ -0,0 +1,16 @@
import dashboard from './dashboard';
dashboard.dataSources.push({
id: "samples",
type: "Sample",
params: {
samples: {
values: [
{ id: "value1", count: 60 },
{ id: "value2", count: 10 },
{ id: "value3", count: 30 }
]
}
}
});
export default dashboard;

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

@ -0,0 +1,23 @@
import dashboard from './samples';
dashboard.elements.push({
id: 'table',
type: 'Table',
size: { w: 1, h: 1 },
title: 'Table',
subtitle: 'Table',
dependencies: { values: 'samples:values' },
props: {
cols: [
{
header: 'Conversation Id',
field: 'id'
},
{
header: 'Count',
field: 'count'
}
]
}
});
export default dashboard;

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

@ -0,0 +1,17 @@
import dashboard from './dashboard';
dashboard.dataSources.push({
id: 'timespan',
type: 'Constant',
params: {
values: ['24 hours', '1 week', '1 month'],
selectedValue: '1 month'
},
calculated: (state, dependencies) => {
var queryTimespan = state.selectedValue === '24 hours' ? 'PT24H' : state.selectedValue === '1 week' ? 'P7D' : 'P30D';
var granularity = state.selectedValue === '24 hours' ? '5m' : state.selectedValue === '1 week' ? '1d' : '1d';
return { queryTimespan, granularity };
}
});
export default dashboard;

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

@ -0,0 +1,13 @@
import * as nock from 'nock';
function mockRequests() {
nock('http://localhost')
.get('/auth/account')
.reply(200, {
account: 'account'
});
}
export {
mockRequests
};

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

@ -1,7 +1,10 @@
import * as nock from 'nock';
import dashboardMock from '../../dashboards/application-insights';
import query24HResponseMock from './query.24h.mock';
import query30DResponseMock from './query.30d.mock';
import { appInsightsUri, appId, apiKey } from '../../../data-sources/plugins/ApplicationInsights/common';
import { appInsightsUri } from '../../../../data-sources/plugins/ApplicationInsights/common';
const { appId, apiKey } = dashboardMock.config.connections['application-insights'];
/**
* Mocking application insights requets
@ -13,9 +16,11 @@ function mockRequests() {
"x-api-key": apiKey
}
})
.get(`/${appId}/query?timespan=PT24H&query=customEvents`)
.post(`/${appId}/query?timespan=PT24H`)
.delay(100)
.reply(200, query24HResponseMock)
.get(`/${appId}/query?timespan=P30D&query=customEvents`)
.post(`/${appId}/query?timespan=P30D`)
.delay(100)
.reply(200, query30DResponseMock);
}

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

@ -0,0 +1,33 @@
import * as nock from 'nock';
import dashboard from '../dashboards/dashboard';
function mockRequests() {
nock('http://localhost')
.get('/api/dashboards')
.reply(200, `
(function (window) {
var dashboardTemplate = (function () {
return ${JSON.stringify(dashboard)};
})();
window.dashboardTemplates = window.dashboardTemplates || [];
window.dashboardTemplates.push(dashboardTemplate);
})(window);
(function (window) {
var dashboard = (function () {
return ${JSON.stringify(dashboard)};
})();
window.dashboardDefinitions = window.dashboardDefinitions || [];
window.dashboardDefinitions.push(dashboard);
})(window);
(function (window) {
var dashboard = (function () {
return ${JSON.stringify(dashboard)};
})();
window.dashboard = dashboard || null;
})(window);
`);
}
export {
mockRequests
};

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

@ -1,23 +0,0 @@
export default {
title: 'Table',
subtitle: 'Table',
dependencies: { values: 'data:someJsonValues' },
actions: { },
props: {
checkboxes: false,
rowClassNameField: '',
cols: [{
header: 'Conversation Id',
field: 'id'
}, {
header: 'Count',
field: 'count'
}, {
type: 'button',
value: 'chat',
onClick: 'openMessagesDialog'
}
] as any
},
layout: { "x": 1, "y": 1, "w": 1, "h": 1 }
}

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

@ -0,0 +1,19 @@
import AccountStore from "../../stores/AccountStore";
import AccountActions from "../../actions/AccountActions";
import { mockRequests } from '../mocks/requests/account';
describe('Data Source: Samples', () => {
beforeAll(() => {
mockRequests();
})
it ('Testing AccountActions', (done) => {
AccountStore.listen((state) => {
expect(state).toHaveProperty('account');
done();
});
AccountActions.updateAccount();
});
});

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

@ -0,0 +1,26 @@
import ConfigurationsStore from "../../stores/ConfigurationsStore";
import ConfigurationsActions from "../../actions/ConfigurationsActions";
import { mockRequests } from '../mocks/requests/configuration';
describe('Data Source: ConfigurationsActions', () => {
beforeAll(() => {
mockRequests();
})
it ('Testing load', (done) => {
ConfigurationsStore.listen((state) => {
try {
expect(state).toHaveProperty('dashboards');
expect(state).toHaveProperty('templates');
done();
} catch (e) {
done(e);
throw e;
}
});
ConfigurationsActions.loadConfiguration();
});
});

15
src/tests/utils/setup.ts Normal file
Просмотреть файл

@ -0,0 +1,15 @@
import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources';
function setupTests(dashboardMock: IDashboardConfig, done?: () => void): IDataSourceDictionary {
DataSourceConnector.createDataSources(dashboardMock, dashboardMock.config.connections);
let dataSources = DataSourceConnector.getDataSources();
// Waiting for all defered functions to complete their execution
done && setTimeout(done, 100);
return dataSources;
}
export {
setupTests
};

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

@ -62,6 +62,7 @@ interface IDashboardConfig extends IDataSourceContainer, IElementsContainer {
id: string,
name: string,
icon?: string,
logo?: string,
url: string,
description?: string,
html?: string,

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

@ -12,4 +12,4 @@ export default {
ago: (date: Date): string => {
return moment(date).fromNow();
}
}
};

112
yarn.lock
Просмотреть файл

@ -8,6 +8,12 @@
dependencies:
"@types/react" "*"
"@types/form-data@*":
version "0.0.33"
resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-0.0.33.tgz#c9ac85b2a5fd18435b8c85d9ecb50e6d6c893ff8"
dependencies:
"@types/node" "*"
"@types/history@^3":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@types/history/-/history-3.2.1.tgz#0039ab0e0be2a0cc22bac171d27a44588103d123"
@ -26,7 +32,11 @@
dependencies:
"@types/node" "*"
"@types/node@*", "@types/node@^7.0.8":
"@types/node@*", "@types/node@^7.0.10":
version "7.0.18"
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.18.tgz#cd67f27d3dc0cfb746f0bdd5e086c4c5d55be173"
"@types/node@^7.0.8":
version "7.0.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.8.tgz#25e4dd804b630c916ae671233e6d71f6ce18124a"
@ -53,6 +63,19 @@
version "15.0.16"
resolved "https://registry.yarnpkg.com/@types/react/-/react-15.0.16.tgz#78e39511a9cfcabf7f74ecd55180522f4290a0c1"
"@types/request@^0.0.42":
version "0.0.42"
resolved "https://registry.yarnpkg.com/@types/request/-/request-0.0.42.tgz#e47a53bf0b130464854fb693297746a0c0479c31"
dependencies:
"@types/form-data" "*"
"@types/node" "*"
"@types/uuid@^2.0.29":
version "2.0.29"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-2.0.29.tgz#939a198ab73567f811ab84f670d2be9c25addd41"
dependencies:
"@types/node" "*"
abab@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d"
@ -82,6 +105,19 @@ acorn@^4.0.4:
version "4.0.11"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0"
adal-node@^0.1.17:
version "0.1.22"
resolved "https://registry.yarnpkg.com/adal-node/-/adal-node-0.1.22.tgz#be77bd2ea8a8a0a03184b426118fd16f79f63725"
dependencies:
async ">=0.6.0"
date-utils "*"
jws "3.x.x"
node-uuid "1.4.7"
request ">= 2.52.0"
underscore ">= 1.3.1"
xmldom ">= 0.1.x"
xpath.js "~1.0.5"
ajv@^4.9.1:
version "4.11.5"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.5.tgz#b6ee74657b993a01dce44b7944d56f485828d5bd"
@ -261,7 +297,11 @@ async-foreach@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
async@1.5.2, async@^1.3.0, async@^1.4.0, async@^1.4.2, async@^1.5.0, async@^1.5.2:
async@0.2.7, async@~0.2.6:
version "0.2.7"
resolved "https://registry.yarnpkg.com/async/-/async-0.2.7.tgz#44c5ee151aece6c4bf5364cfc7c28fe4e58f18df"
async@1.5.2, async@>=0.6.0, async@^1.3.0, async@^1.4.0, async@^1.4.2, async@^1.5.0, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
@ -275,10 +315,6 @@ async@^2.1.4:
dependencies:
lodash "^4.14.0"
async@~0.2.6:
version "0.2.10"
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@ -1217,6 +1253,10 @@ date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
date-utils@*:
version "1.2.21"
resolved "https://registry.yarnpkg.com/date-utils/-/date-utils-1.2.21.tgz#61fb16cdc1274b3c9acaaffe9fc69df8720a2b64"
debug@2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.1.tgz#79855090ba2c4e3115cc7d8769491d58f0491351"
@ -2282,7 +2322,7 @@ is-binary-path@^1.0.0:
dependencies:
binary-extensions "^1.0.0"
is-buffer@^1.0.2:
is-buffer@^1.0.2, is-buffer@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
@ -2827,7 +2867,7 @@ jwk-to-pem@^1.2.6:
elliptic "^6.2.3"
safe-buffer "^5.0.1"
jws@^3.1.3:
jws@3.x.x, jws@^3.1.3:
version "3.1.4"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2"
dependencies:
@ -3249,7 +3289,7 @@ minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
dependencies:
minimist "0.0.8"
moment@^2.10.6, moment@^2.18.0:
moment@^2.10.6, moment@^2.14.1, moment@^2.18.0:
version "2.18.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.0.tgz#6cfec6a495eca915d02600a67020ed994937252c"
@ -3263,6 +3303,34 @@ morgan@^1.8.1:
on-finished "~2.3.0"
on-headers "~1.0.1"
ms-rest-azure@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms-rest-azure/-/ms-rest-azure-2.1.2.tgz#9774b1d4141c8a3a250ae5da36fc2654542be738"
dependencies:
"@types/node" "^7.0.10"
"@types/uuid" "^2.0.29"
adal-node "^0.1.17"
async "0.2.7"
moment "^2.14.1"
ms-rest "^2.2.0"
uuid "^3.0.1"
ms-rest@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/ms-rest/-/ms-rest-2.2.0.tgz#5f2507522f1585e26666815588dbacbcec7fb79f"
dependencies:
"@types/node" "^7.0.10"
"@types/request" "^0.0.42"
"@types/uuid" "^2.0.29"
duplexer "~0.1.1"
is-buffer "^1.1.5"
is-stream "^1.1.0"
moment "^2.14.1"
request "^2.74.0"
through "~2.3.4"
tunnel "~0.0.2"
uuid "^3.0.1"
ms@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
@ -3440,6 +3508,10 @@ node-sass@^4.5.0:
sass-graph "^2.1.1"
stdout-stream "^1.4.0"
node-uuid@1.4.7:
version "1.4.7"
resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.7.tgz#6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f"
nodent-runtime@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/nodent-runtime/-/nodent-runtime-3.0.4.tgz#a801ecb7bb0f6c39a69b24cc2fa370cfa8b492da"
@ -4633,7 +4705,7 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
request@2, request@^2.61.0, request@^2.72.0, request@^2.79.0:
request@2, "request@>= 2.52.0", request@^2.61.0, request@^2.72.0, request@^2.74.0, request@^2.79.0:
version "2.81.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
dependencies:
@ -5261,7 +5333,7 @@ tslint-react@^2.0.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tslint-react/-/tslint-react-2.5.0.tgz#94cd6143f6ae825c47f242c5146fc89caeadeae2"
tslint@^4.0.2:
tslint@^4.0.0, tslint@^4.0.2:
version "4.5.1"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-4.5.1.tgz#05356871bef23a434906734006fc188336ba824b"
dependencies:
@ -5289,6 +5361,10 @@ tunnel-agent@^0.6.0:
dependencies:
safe-buffer "^5.0.1"
tunnel@~0.0.2:
version "0.0.4"
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.4.tgz#2d3785a158c174c9a16dc2c046ec5fc5f1742213"
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
@ -5353,6 +5429,10 @@ uid-safe@~2.1.4:
dependencies:
random-bytes "~1.0.0"
"underscore@>= 1.3.1":
version "1.8.3"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
uniq@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
@ -5472,7 +5552,7 @@ uuid@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
uuid@^3.0.0:
uuid@^3.0.0, uuid@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"
@ -5743,6 +5823,14 @@ xml-name-validator@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
"xmldom@>= 0.1.x":
version "0.1.27"
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
xpath.js@~1.0.5:
version "1.0.7"
resolved "https://registry.yarnpkg.com/xpath.js/-/xpath.js-1.0.7.tgz#7e94627f541276cbc6a6b02b5d35e9418565b3e4"
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"