Merge azure
This commit is contained in:
Коммит
3eceafc49f
|
@ -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
|
||||
|
|
|
@ -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 & 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();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
};
|
|
@ -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
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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче