loading configuration dynamically + edit

This commit is contained in:
morsh 2017-03-27 21:01:53 +03:00
Родитель 4703576429
Коммит 270d1f91e6
22 изменённых файлов: 615 добавлений и 282 удалений

5
.gitignore поставляемый
Просмотреть файл

@ -20,4 +20,7 @@ yarn-error.log*
*.css
# generated merge temp files
*.orig
*.orig
# private files
*.private.*

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

@ -0,0 +1,30 @@
{
// Use IntelliSense to learn about possible Node.js debug attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Server",
"program": "${workspaceRoot}\\server\\index.js",
"outFiles": []
},
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceRoot}\\client:start",
"outFiles": []
},
{
"type": "node",
"request": "attach",
"name": "Attach to Process",
"address": "localhost",
"port": 5858,
"outFiles": []
}
]
}

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

@ -22,6 +22,7 @@
"dependencies": {
"alt": "^0.18.6",
"alt-utils": "^1.0.0",
"body-parser": "^1.17.1",
"lodash": "^4.17.4",
"material-colors": "^1.2.5",
"moment": "^2.18.0",

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

@ -3,8 +3,13 @@ const express = require('express');
const morgan = require('morgan');
const path = require('path');
const fs = require('fs');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json()); // to support JSON-encoded bodies
app.use(bodyParser.urlencoded({ // to support URL-encoded bodies
extended: true
}));
// Setup logger
app.use(morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] :response-time ms'));
@ -12,20 +17,58 @@ app.use(morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:htt
// Serve static assets
app.use(express.static(path.resolve(__dirname, '..', 'build')));
app.get('/api/config.js', (req, res) => {
fs.readFile(path.join(__dirname, 'dashboards', 'bot-framework.js'), 'utf8', (err, data) => {
function getFiles(dir, files_) {
files_ = files_ || [];
var files = fs.readdirSync(dir);
for (var i in files){
var name = dir + '/' + files[i];
if (fs.statSync(name).isDirectory()){
getFiles(name, files_);
} else {
files_.push(name);
}
}
return files_;
}
app.get('/api/dashboard.js', (req, res) => {
let privateDashboard = path.join(__dirname, 'dashboards', 'dashboard.private.js');
let preconfDashboard = path.join(__dirname, 'dashboards', 'preconfigured', 'bot-framework.js');
let dashboardPath = fs.existsSync(privateDashboard) ? privateDashboard : preconfDashboard;
fs.readFile(dashboardPath, 'utf8', (err, data) => {
if (err) throw err;
// Ensuing this dashboard is loaded into the dashboards array on the page
data += `
window.dashboards = window.dashboards || [];
window.dashboards.push(dashboard);
let script = `
(function (window) {
var dashboard = (function () {
${data}
})();
window.dashboards = window.dashboards || [];
window.dashboards.push(dashboard);
})(window);
`;
res.send(data);
res.send(script);
});
});
app.post('/api/dashboard.js', (req, res) => {
var content = (req.body && req.body.script) || '';
console.dir(content);
fs.writeFile(path.join(__dirname, 'dashboards', 'dashboard.private.js'), content, err => {
if (err) {
console.error(err);
return res.end(err);
}
res.end(content);
})
});
// Always return the main index.html, so react-router render the route in the client
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, '..', 'build', 'index.html'));

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

@ -1,11 +1,6 @@
var dashboard = {
return {
config: {
connections: {
"application-insights": {
appId: '4d567b3c-e52c-4139-8e56-8e573e55a06c',
apiKey: 'tn6hhxs60afz4yzd7cp2nreph1wja3ecxtflq8rs'
}
},
connections: { },
layout: {
isDraggable: true,
isResizable: true,

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

@ -1,7 +1,9 @@
import alt, { AbstractActions } from '../alt';
import * as request from 'xhr-request';
interface IConfigurationsActions {
loadConfiguration(): any;
saveConfiguration(dashboard: IDashboardConfig): any;
failure(error: any): void;
}
@ -10,30 +12,34 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
super(alt);
}
private getScript(source: string, callback?: () => void): void {
let script: any = document.createElement('script');
let prior = document.getElementsByTagName('script')[0];
script.async = 1;
prior.parentNode.insertBefore(script, prior);
script.onload = script.onreadystatechange = (_, isAbort) => {
if(isAbort || !script.readyState || /loaded|complete/.test(script.readyState) ) {
script.onload = script.onreadystatechange = null;
script = undefined;
if(!isAbort) { if(callback) callback(); }
}
};
script.src = source;
}
loadConfiguration() {
return (dispatcher: (dashboard: IDashboardConfig) => void) => {
// request('/api/dashboard.js',
// (error: any, scriptString: string) => {
// // do something
// debugger;
// if (!scriptString || !scriptString.length) {
// return this.failure(new Error('Could not load dashboard configuration from server'));
// }
// let dashboard: IDashboardConfig = null;
// eval(`dashboard = (function() { ${scriptString} })()`); /* tslint:disable-line */
// if (!dashboard) {
// return this.failure(new Error('Could not load configuration from script: ' + scriptString));
// }
// this.fixCalculatedProperties(dashboard);
// return dispatcher(dashboard);
// });
this.getScript('/api/config.js', () => {
let dashboards: IDashboardConfig[] = (window as any)["dashboards"];
this.getScript('/api/dashboard.js', () => {
let dashboards: IDashboardConfig[] = (window as any)['dashboards'];
if (!dashboards || !dashboards.length) {
return this.failure(new Error('Could not load configuration'));
@ -45,9 +51,158 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
};
}
saveConfiguration(dashboard: IDashboardConfig) {
return (dispatcher: (dashboard: IDashboardConfig) => void) => {
let stringDashboard = this.objectToString(dashboard);
request('/api/dashboard.js', {
method: 'POST',
json: true,
body: { script: 'return ' + stringDashboard }
},
(error: any, json: any) => {
if (error) {
return this.failure(error);
}
return dispatcher(json);
}
);
};
}
failure(error: any) {
return { error };
}
private getScript(source: string, callback?: () => void): void {
let script: any = document.createElement('script');
let prior = document.getElementsByTagName('script')[0];
script.async = 1;
prior.parentNode.insertBefore(script, prior);
script.onload = script.onreadystatechange = (_, isAbort) => {
if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState) ) {
script.onload = script.onreadystatechange = null;
script = undefined;
if (!isAbort) { if (callback) { callback(); } }
}
};
script.src = source;
}
/**
* Convret a json object with functions to string
* @param obj an object with functions to convert to string
*/
private objectToString(obj: Object, indent: number = 0, lf: boolean = false): string {
let result = ''; //(lf ? '\n' : '') + '\t'.repeat(indent);
let sind = '\t'.repeat(indent);
let objectType = (Array.isArray(obj) && 'array') || typeof obj;
switch (objectType) {
case 'object': {
// Iterating through all values in object
let objectValue = '';
Object.keys(obj).forEach((key: string, idx: number) => {
if (idx > 0) { objectValue += ',\n'; }
let value = this.objectToString(obj[key], indent + 1, true);
// if key contains '.' or '-'
let skey = key.search(/\.|\-/g) >= 0 ? `"${key}"` : `${key}`;
objectValue += `${sind}\t${skey}: ${value}`;
});
result += `{\n${objectValue}\n${sind}}`;
break;
}
case 'string':
let stringValue = obj.toString().replace(/\"/g, '\\"');
result += `"${stringValue}"`;
break;
case 'function': {
result += obj.toString();
break;
}
case 'number':
case 'boolean': {
result += `${obj}`;
break;
}
case 'array': {
let arrayValue = '';
(obj as any[]).forEach((value: any, idx: number) => {
arrayValue += idx > 0 ? ',' : '';
arrayValue += this.objectToString(value, indent + 1, true);
});
result += `[${arrayValue}]`;
break;
}
default:
throw new Error('An unhandled type was found: ' + typeof objectType);
}
return result;
}
/**
* convert a string to object (with strings)
* @param str a string to turn to object with functions
*/
private stringToObject(str: string): Object {
// we doing this recursively so after the first one it will be an object
let parsedString: Object;
try {
parsedString = JSON.parse(`{${str}}`);
} catch (e) {
parsedString = str;
}
var obj = {};
for (var i in parsedString) {
if (typeof parsedString[i] === 'string') {
if (parsedString[i].substring(0, 8) === 'function') {
eval('obj[i] = ' + parsedString[i] ); /* tslint:disable-line */
} else {
obj[i] = parsedString[i];
}
} else if (typeof parsedString[i] === 'object') {
obj[i] = this.stringToObject(parsedString[i]);
}
}
return obj;
}
private fixCalculatedProperties(dashboard: IDashboardConfig): void {
dashboard.dataSources.forEach(dataSource => {
let calculated: string = dataSource.calculated as any;
if (calculated) {
if (!calculated.startsWith('function(){return')) {
throw new Error('calculated function format is not recognized: ' + calculated);
}
calculated = calculated.substr('function(){return'.length, calculated.length - 'function(){return'.length - 1);
eval('dataSource.calculated = ' + calculated); /* tslint:disable-line */
}
})
}
}
const configurationsActions = alt.createActions<IConfigurationsActions>(ConfigurationsActions);

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

@ -13,97 +13,44 @@ import ConnectionsStore from '../../stores/ConnectionsStore';
import ConnectionsActions from '../../actions/ConnectionsActions';
interface IConfigDashboardState {
dashboard?: IDashboardConfig;
connections: IDictionary;
error: string;
}
export default class ConfigDashboard extends React.Component<null, IConfigDashboardState> {
interface IConfigDashboardProps {
dashboard: IDashboardConfig;
connections: IDictionary;
}
state = {
dashboard: null,
export default class ConfigDashboard extends React.Component<IConfigDashboardProps, IConfigDashboardState> {
state: IConfigDashboardState = {
connections: {},
error: null
};
dataSources: IDataSourceDictionary = {};
constructor(props: any) {
super(props);
this.loadParams = this.loadParams.bind(this);
this.onChange = this.onChange.bind(this);
this.state.connections = ConnectionsStore.getState();
this.onSave = this.onSave.bind(this);
this.onSaveGoToDashboard = this.onSaveGoToDashboard.bind(this);
ConfigurationsActions.loadConfiguration();
}
componentDidMount() {
ConnectionsStore.listen(this.onChange);
let { dashboard } = ConfigurationsStore.getState();
if (dashboard) {
DataSourceConnector.createDataSources(dashboard, this.dataSources);
this.setState({ dashboard });
}
ConfigurationsStore.listen(state => {
let { dashboard } = state;
DataSourceConnector.createDataSources(dashboard, this.dataSources);
this.setState({ dashboard });
});
}
componentWillUnmount() {
ConnectionsStore.unlisten(this.onChange);
}
private loadParams(): any {
var requiredParameters = {};
_.values(this.dataSources).forEach(dataSource => {
// If no connection requirements were set, return
if (!dataSource.plugin.connection) {
return;
}
if (!connections[dataSource.plugin.connection]) {
throw new Error(`No connection names ${dataSource.plugin.connection} was defined`);
}
var connectionType = connections[dataSource.plugin.connection];
requiredParameters[dataSource.plugin.connection] = {};
connectionType.params.forEach(param => { requiredParameters[dataSource.plugin.connection][param] = null });
// Connection type is already defined - check params
let { dashboard } = this.state;
if (dashboard.config.connections[dataSource.plugin.connection]) {
var connectionParams = dashboard.config.connections[dataSource.plugin.connection];
// Checking that all param definitions are defined
connectionType.params.forEach(param => {
requiredParameters[dataSource.plugin.connection][param] = connectionParams[param];
});
}
});
return requiredParameters;
}
onChange(state) {
this.setState(state);
}
onParamChange(connectionKey, paramKey, value) {
//debugger;
this.state.connections[connectionKey][paramKey] = value;
this.setState({ connections: this.state.connections });
let { connections } = this.state;
connections[connectionKey] = connections[connectionKey] || {};
connections[connectionKey][paramKey] = value;
this.setState({ connections });
}
onSave() {
let { dashboard } = this.props;
let { connections } = this.state;
dashboard.config.connections = connections;
ConfigurationsActions.saveConfiguration(dashboard);
}
onSaveGoToDashboard() {
@ -112,11 +59,11 @@ export default class ConfigDashboard extends React.Component<null, IConfigDashbo
render() {
if (!this.state.dashboard) {
if (!this.props.dashboard) {
return null;
}
let connections = this.loadParams();
let { connections } = this.props;
let { error } = this.state;
return (
@ -130,7 +77,7 @@ export default class ConfigDashboard extends React.Component<null, IConfigDashbo
<TextField
id="paramKey"
label={paramKey}
defaultValue={connections[connectionKey][paramKey]}
defaultValue={connections[connectionKey] && connections[connectionKey][paramKey] || ''}
lineDirection="center"
placeholder="Fill in required connection parameter"
className="md-cell md-cell--bottom"

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

@ -0,0 +1,130 @@
import * as React from 'react';
import Toolbar from 'react-md/lib/Toolbars';
import { Spinner } from '../Spinner';
import * as ReactGridLayout from 'react-grid-layout';
var ResponsiveReactGridLayout = ReactGridLayout.Responsive;
var WidthProvider = ReactGridLayout.WidthProvider;
ResponsiveReactGridLayout = WidthProvider(ResponsiveReactGridLayout);
import ElementConnector from '../ElementConnector';
import { loadDialogsFromDashboard } from '../generic/Dialogs';
import ConfigurationsActions from '../../actions/ConfigurationsActions';
import ConfigurationsStore from '../../stores/ConfigurationsStore';
interface IDashboardState {
mounted?: boolean;
currentBreakpoint?: string;
layouts?: ILayouts;
grid?: any;
}
interface IDashboardProps {
dashboard?: IDashboardConfig;
}
export default class Dashboard extends React.Component<IDashboardProps, IDashboardState> {
layouts = {};
state = {
currentBreakpoint: 'lg',
mounted: false,
layouts: { },
grid: null
};
componentDidMount() {
let { dashboard } = this.props;
let { mounted } = this.state;
if (dashboard && !mounted) {
const layout = dashboard.config.layout;
// For each column, create a layout according to number of columns
var layouts = ElementConnector.loadLayoutFromDashboard(dashboard, dashboard);
this.layouts = layouts;
this.setState({
mounted: true,
layouts: { lg: layouts['lg'] },
grid: {
className: 'layout',
rowHeight: layout.rowHeight || 30,
cols: layout.cols,
breakpoints: layout.breakpoints,
verticalCompact: false
}
});
}
}
componentDidUpdate() {
this.componentDidMount();
}
onBreakpointChange = (breakpoint) => {
var layouts = this.state.layouts;
layouts[breakpoint] = layouts[breakpoint] || this.layouts[breakpoint];
this.setState({
currentBreakpoint: breakpoint,
layouts: layouts
});
}
onLayoutChange = (layout, layouts) => {
// this.props.onLayoutChange(layout, layouts);
var breakpoint = this.state.currentBreakpoint;
var newLayouts = this.state.layouts;
newLayouts[breakpoint] = layout;
this.setState({
layouts: newLayouts
});
}
render() {
let { dashboard } = this.props;
var { currentBreakpoint, grid } = this.state;
var layout = this.state.layouts[currentBreakpoint];
if (!grid) {
return null;
}
// Creating visual elements
var elements = ElementConnector.loadElementsFromDashboard(dashboard, layout);
// Creating filter elements
var { filters, /*additionalFilters*/ } = ElementConnector.loadFiltersFromDashboard(dashboard);
// Loading dialogs
var dialogs = loadDialogsFromDashboard(dashboard);
return (
<div style={{ width: '100%' }}>
<Toolbar>
{filters}
<Spinner />
</Toolbar>
<ResponsiveReactGridLayout
{...grid}
layouts={this.state.layouts}
onBreakpointChange={this.onBreakpointChange}
onLayoutChange={this.onLayoutChange}
// 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}
</ResponsiveReactGridLayout>
{dialogs}
</div>
);
}
}

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

@ -17,16 +17,16 @@ var WidthProvider = ReactGridLayout.WidthProvider;
ResponsiveReactGridLayout = WidthProvider(ResponsiveReactGridLayout);
interface IDialogProps {
dialogData: IDialog
dashboard: IDashboardConfig
dialogData: IDialog;
dashboard: IDashboardConfig;
}
interface IDialogState {
dialogId?: string
dialogArgs?: IDictionary
mounted?: boolean
currentBreakpoint?: string
layouts?: ILayouts
dialogId?: string;
dialogArgs?: IDictionary;
mounted?: boolean;
currentBreakpoint?: string;
layouts?: ILayouts;
}
export default class Dialog extends React.PureComponent<IDialogProps, IDialogState> {
@ -48,10 +48,10 @@ export default class Dialog extends React.PureComponent<IDialogProps, IDialogSta
selectedValue: null
}
};
DataSourceConnector.createDataSources({ dataSources: [ dialogDS ] }, this.dataSources);
DataSourceConnector.createDataSources({ dataSources: [ dialogDS ] }, this.props.dashboard.config.connections);
// Adding other data sources
DataSourceConnector.createDataSources(this.props.dialogData, this.dataSources);
DataSourceConnector.createDataSources(this.props.dialogData, this.props.dashboard.config.connections);
var layouts = ElementConnector.loadLayoutFromDashboard(this.props.dialogData, this.props.dashboard);
@ -62,8 +62,6 @@ export default class Dialog extends React.PureComponent<IDialogProps, IDialogSta
componentDidMount() {
this.setState({ mounted: true });
DialogsStore.listen(this.onChange);
DataSourceConnector.connectDataSources(this.dataSources);
}
componentDidUpdate() {

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

@ -9,6 +9,7 @@ export interface IDataSource {
plugin : IDataSourcePlugin;
action: any;
store: any;
initialized: boolean;
}
export interface IDataSourceDictionary {
@ -24,7 +25,7 @@ export class DataSourceConnector {
private static dataSources: IDataSourceDictionary = {};
static createDataSource(dataSourceConfig) {
static createDataSource(dataSourceConfig: any, connections: IConnections) {
var config = dataSourceConfig || {};
if (!config.id || !config.type) {
@ -34,7 +35,7 @@ export class DataSourceConnector {
// Dynamically load the plugin from the plugins directory
var pluginPath = './plugins/' + config.type;
var PluginClass = require(pluginPath);
var plugin : any = new PluginClass.default(config);
var plugin : any = new PluginClass.default(config, connections);
// Creating actions class
var ActionClass = DataSourceConnector.createActionClass(plugin);
@ -47,52 +48,56 @@ export class DataSourceConnector {
config,
plugin,
action: ActionClass,
store: StoreClass
store: StoreClass,
initialized: false
}
return DataSourceConnector.dataSources[config.id];
}
static createDataSources(dsContainer: IDataSourceContainer, containerDataSources: IDataSourceDictionary) {
static createDataSources(dsContainer: IDataSourceContainer, connections: IConnections) {
dsContainer.dataSources.forEach(source => {
var dataSource = DataSourceConnector.createDataSource(source);
containerDataSources[dataSource.id] = dataSource;
var dataSource = DataSourceConnector.createDataSource(source, connections);
DataSourceConnector.connectDataSource(dataSource);
});
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);
}
});
});
}
static connectDataSources(dataSources: IDataSourceDictionary) {
// Connect sources and dependencies
var sourcesIDs = Object.keys(dataSources);
sourcesIDs.forEach(sourceDSId => {
var sourceDS = dataSources[sourceDSId];
sourceDS.store.listen((state) => {
sourcesIDs.forEach(checkDSId => {
var checkDS = dataSources[checkDSId];
var dependencies = checkDS.plugin.getDependencies() || {};
let connected = _.find(_.keys(dependencies), dependencyKey => {
let dependencyValue = dependencies[dependencyKey] || '';
return (dependencyValue === sourceDSId || dependencyValue.startsWith(sourceDSId + ':'));
})
if (connected) {
// Todo: add check that all dependencies are met
checkDS.action.updateDependencies.defer(state);
}
});
});
});
static initializeDataSources() {
// Call initalize methods
sourcesIDs.forEach(sourceDSId => {
var sourceDS = dataSources[sourceDSId];
Object.keys(this.dataSources).forEach(sourceDSId => {
var sourceDS = this.dataSources[sourceDSId];
if (sourceDS.initialized) { return; }
if (typeof sourceDS.action['initialize'] === 'function') {
sourceDS.action.initialize.defer();
}
sourceDS.initialized = true;
});
}
@ -158,6 +163,10 @@ export class DataSourceConnector {
}
}
static getDataSources(): IDataSourceDictionary {
return this.dataSources;
}
private static createActionClass(plugin: IDataSourcePlugin) : any {
class NewActionClass {
constructor() {}

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

@ -23,10 +23,9 @@ export default class ApplicationInsightsEvents extends DataSourcePlugin {
type = 'ApplicationInsights-Events';
defaultProperty = 'values';
connection = 'application-insights';
constructor(options: IEventsOptions) {
super(options);
constructor(options: IEventsOptions, connections: IDict<IStringDictionary>) {
super(options, connections);
// var props = this._props;
// var params: any = props.params;

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

@ -2,8 +2,11 @@
//import * as $ from 'jquery';
import * as request from 'xhr-request';
import * as _ from 'lodash';
import {DataSourcePlugin, IDataSourceOptions} from '../DataSourcePlugin';
import { appInsightsUri, appId, apiKey } from './common';
import { DataSourcePlugin, IDataSourceOptions } from '../DataSourcePlugin';
import { appInsightsUri } from './common';
import ApplicationInsightsConnection from '../../connections/application-insights';
let connectionType = new ApplicationInsightsConnection();
interface IQueryOptions extends IDataSourceOptions {
query: string
@ -14,13 +17,13 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin {
type = 'ApplicationInsights-Query';
defaultProperty = 'values';
connection = 'application-insights';
connectionType = connectionType.type;
/**
* @param options - Options object
*/
constructor(options: IQueryOptions) {
super(options);
constructor(options: IQueryOptions, connections: IDict<IStringDictionary>) {
super(options, connections);
var props = this._props;
var params: any = props.params;
@ -50,6 +53,15 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin {
};
}
// Validate connection
let connection = this.getConnection();
let { appId, apiKey } = connection;
if (!connection || !apiKey || !appId) {
return (dispatch) => {
return dispatch();
};
}
var { queryTimespan } = dependencies;
var params: any = this._props.params;

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

@ -14,8 +14,8 @@ export default class Constant extends DataSourcePlugin {
type = 'Constant';
defaultProperty = 'selectedValue';
constructor(options: IConstantOptions) {
super(options);
constructor(options: IConstantOptions, connections: IDict<IStringDictionary>) {
super(options, connections);
var props = this._props;
var params = options.params;

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

@ -13,7 +13,7 @@ export interface IDataSourcePlugin {
type: string;
defaultProperty: string;
connection: string;
connectionType: string;
_props: {
id: string,
@ -32,15 +32,15 @@ export interface IDataSourcePlugin {
getParamKeys(): string[];
getParams(): IDictionary;
getCalculated(): ICalculated;
getConnection(): IStringDictionary;
}
export abstract class DataSourcePlugin implements IDataSourcePlugin {
abstract type: string;
abstract defaultProperty: string;
connectionType: string = null;
connection: string = null;
_props = {
id: '',
dependencies: {} as any,
@ -53,7 +53,7 @@ export abstract class DataSourcePlugin implements IDataSourcePlugin {
/**
* @param {DataSourcePlugin} options - Options object
*/
constructor(options: IDictionary) {
constructor(options: IDictionary, protected connections: IConnections = {}) {
var props = this._props;
props.id = options.id;
@ -62,6 +62,8 @@ export abstract class DataSourcePlugin implements IDataSourcePlugin {
props.actions.push.apply(props.actions, options.actions || []);
props.params = options.params || {};
props.calculated = options.calculated || {};
this.updateDependencies = this.updateDependencies.bind(this);
}
bind (actionClass: any) {
@ -69,6 +71,15 @@ export abstract class DataSourcePlugin implements IDataSourcePlugin {
actionClass._props = this._props;
}
updateConnections(connections: IConnections) {
this.connections = connections;
}
getConnection(): IConnection {
return (this.connections && this.connections[this.connectionType]) || {};
}
abstract updateDependencies (dependencies: IDictionary, args: IDictionary, callback: () => void): void;
/**

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

@ -1,39 +1,23 @@
import * as React from 'react';
import Toolbar from 'react-md/lib/Toolbars';
import { Spinner } from '../components/Spinner';
import * as ReactGridLayout from 'react-grid-layout';
var ResponsiveReactGridLayout = ReactGridLayout.Responsive;
var WidthProvider = ReactGridLayout.WidthProvider;
ResponsiveReactGridLayout = WidthProvider(ResponsiveReactGridLayout);
import { DataSourceConnector, IDataSourceDictionary } from '../data-sources';
import ElementConnector from '../components/ElementConnector';
import { loadDialogsFromDashboard } from '../components/generic/Dialogs';
import DashboardComponent from '../components/Dashboard';
import ConfigDashboard from '../components/ConfigDashboard';
import ConfigurationsActions from '../actions/ConfigurationsActions';
import ConfigurationsStore from '../stores/ConfigurationsStore';
interface IDashboardState {
dashboard?: IDashboardConfig;
mounted?: boolean;
currentBreakpoint?: string;
layouts?: ILayouts;
grid?: any;
connections?: IConnections;
connectionsMissing?: boolean;
}
export default class Dashboard extends React.Component<any, IDashboardState> {
layouts = {};
dataSources: IDataSourceDictionary = {};
state = {
state: IDashboardState = {
dashboard: null,
currentBreakpoint: 'lg',
mounted: false,
layouts: { },
grid: null
connections: {},
connectionsMissing: false
};
constructor(props: any) {
@ -43,96 +27,30 @@ export default class Dashboard extends React.Component<any, IDashboardState> {
}
componentDidMount() {
this.setState({ mounted: true });
let { dashboard } = ConfigurationsStore.getState();
this.setState({ dashboard });
this.setState(ConfigurationsStore.getState());
ConfigurationsStore.listen(state => {
let { dashboard } = state;
const layout = dashboard.config.layout;
DataSourceConnector.createDataSources(dashboard, this.dataSources);
DataSourceConnector.connectDataSources(this.dataSources);
// For each column, create a layout according to number of columns
var layouts = ElementConnector.loadLayoutFromDashboard(dashboard, dashboard);
this.layouts = layouts;
this.setState({
dashboard,
layouts: { lg: layouts['lg'] },
grid: {
className: 'layout',
rowHeight: layout.rowHeight || 30,
cols: layout.cols,
breakpoints: layout.breakpoints,
verticalCompact: false
}
});
})
}
onBreakpointChange = (breakpoint) => {
var layouts = this.state.layouts;
layouts[breakpoint] = layouts[breakpoint] || this.layouts[breakpoint];
this.setState({
currentBreakpoint: breakpoint,
layouts: layouts
});
}
onLayoutChange = (layout, layouts) => {
// this.props.onLayoutChange(layout, layouts);
var breakpoint = this.state.currentBreakpoint;
var newLayouts = this.state.layouts;
newLayouts[breakpoint] = layout;
this.setState({
layouts: newLayouts
this.setState(ConfigurationsStore.getState());
});
}
render() {
var { dashboard, currentBreakpoint, grid } = this.state;
var layout = this.state.layouts[currentBreakpoint];
var { dashboard, connections, connectionsMissing } = this.state;
if (!grid) {
if (!dashboard) {
return null;
}
// Creating visual elements
var elements = ElementConnector.loadElementsFromDashboard(dashboard, layout);
// Creating filter elements
var { filters, /*additionalFilters*/ } = ElementConnector.loadFiltersFromDashboard(dashboard);
// Loading dialogs
var dialogs = loadDialogsFromDashboard(dashboard);
if (connectionsMissing) {
return (
<ConfigDashboard dashboard={dashboard} connections={connections} />
);
}
return (
<div style={{ width: '100%' }}>
<Toolbar>
{filters}
<Spinner />
</Toolbar>
<ResponsiveReactGridLayout
{...grid}
layouts={this.state.layouts}
onBreakpointChange={this.onBreakpointChange}
onLayoutChange={this.onLayoutChange}
// 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}
</ResponsiveReactGridLayout>
{dialogs}
</div>
<DashboardComponent dashboard={dashboard} />
);
}
}

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

@ -1,19 +1,31 @@
import alt, { AbstractStoreModel } from '../alt';
import * as _ from 'lodash';
import connections from '../data-sources/connections';
import { DataSourceConnector, IDataSourceDictionary } from '../data-sources';
import configurationActions from '../actions/ConfigurationsActions';
interface IConfigurationsStoreState {
dashboard: IDashboardConfig;
connections: IDictionary;
connectionsMissing: boolean;
loaded: boolean;
}
class ConfigurationsStore extends AbstractStoreModel<IConfigurationsStoreState> implements IConfigurationsStoreState {
dashboard: IDashboardConfig;
connections: IDictionary;
connectionsMissing: boolean;
loaded: boolean;
constructor() {
super();
this.dashboard = null;
this.connections = {};
this.connectionsMissing = false;
this.loaded = false;
this.bindListeners({
loadConfiguration: configurationActions.loadConfiguration
@ -22,6 +34,52 @@ class ConfigurationsStore extends AbstractStoreModel<IConfigurationsStoreState>
loadConfiguration(dashboard: IDashboardConfig) {
this.dashboard = dashboard;
if (this.dashboard && !this.loaded) {
DataSourceConnector.createDataSources(dashboard, dashboard.config.connections);
this.connections = this.getConnections(dashboard);
// Checking for missing connection params
this.connectionsMissing = Object.keys(this.connections).some(connectionKey => {
var connection = this.connections[connectionKey];
return Object.keys(connection).some(paramKey => !connection[paramKey]);
})
}
}
private getConnections(dashboard: IDashboardConfig): any {
let requiredParameters = {};
let dataSources = DataSourceConnector.getDataSources();
_.values(dataSources).forEach(dataSource => {
// If no connection requirements were set, return
let connectionTypeName = dataSource.plugin.connectionType;
if (!connectionTypeName) {
return;
}
if (!connections[connectionTypeName]) {
throw new Error(`No connection names ${connectionTypeName} was defined`);
}
var connectionType = connections[connectionTypeName];
requiredParameters[connectionTypeName] = {};
connectionType.params.forEach(param => { requiredParameters[connectionTypeName][param] = null });
// Connection type is already defined - check params
if (dashboard.config.connections[connectionTypeName]) {
var connectionParams = dashboard.config.connections[connectionTypeName];
// Checking that all param definitions are defined
connectionType.params.forEach(param => {
requiredParameters[connectionTypeName][param] = connectionParams[param];
});
}
});
return requiredParameters;
}
}

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

@ -11,8 +11,7 @@ describe('Data Source: Application Insights: Query', () => {
beforeAll(() => {
mockRequests();
DataSourceConnector.createDataSources({ dataSources: dataSourcesMock }, dataSources);
DataSourceConnector.connectDataSources(dataSources);
DataSourceConnector.createDataSources({ dataSources: dataSourcesMock }, {});
dataSources.timespan.action.updateDependencies();
});

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

@ -7,10 +7,7 @@ describe('Data Source: Constant', () => {
let dataSources: IDataSourceDictionary = {};
beforeAll(() => {
DataSourceConnector.createDataSources({ dataSources: [ dataSourceMock ]}, dataSources);
DataSourceConnector.connectDataSources(dataSources);
DataSourceConnector.createDataSources({ dataSources: [ dataSourceMock ]}, {});
});
it ('Check basic data == 3 rows', () => {

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

@ -18,8 +18,7 @@ describe('Table', () => {
beforeAll(() => {
DataSourceConnector.createDataSources({ dataSources: [ dataSourceMock ]}, dataSources);
DataSourceConnector.connectDataSources(dataSources);
DataSourceConnector.createDataSources({ dataSources: [ dataSourceMock ]}, {});
table = TestUtils.renderIntoDocument(<Table {...tablePropsMock} />);
TestUtils.isElementOfType(table, 'div');

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

@ -2,9 +2,12 @@ type IDict<T> = { [id: string]: T };
type IDictionary = IDict<any>;
type IStringDictionary = IDict<string>;
type IConnection = IStringDictionary;
type IConnections = IDict<IConnection>;
interface IDashboardConfig extends IDataSourceContainer, IElementsContainer {
config: {
connections: IDictionary,
connections: IConnections,
layout: {
isDraggable?: boolean
isResizable?: boolean

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

@ -24,6 +24,7 @@
"acceptance-tests",
"webpack",
"jest",
"server",
"src/setupTests.ts"
],
"types": [

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

@ -475,6 +475,21 @@ bluebird@^3.4.6:
version "3.5.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
body-parser@^1.17.1:
version "1.17.1"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.1.tgz#75b3bc98ddd6e7e0d8ffe750dfaca5c66993fa47"
dependencies:
bytes "2.4.0"
content-type "~1.0.2"
debug "2.6.1"
depd "~1.1.0"
http-errors "~1.6.1"
iconv-lite "0.4.15"
on-finished "~2.3.0"
qs "6.4.0"
raw-body "~2.2.0"
type-is "~1.6.14"
boolbase@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@ -573,6 +588,10 @@ bytes@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.3.0.tgz#d5b680a165b6201739acb611542aabc2d8ceb070"
bytes@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339"
callsites@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
@ -1147,18 +1166,12 @@ date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
debug@2.6.1:
debug@2.6.1, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.1.tgz#79855090ba2c4e3115cc7d8769491d58f0491351"
dependencies:
ms "0.7.2"
debug@^2.1.0, debug@^2.1.1, debug@^2.2.0:
version "2.6.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d"
dependencies:
ms "0.7.2"
debug@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
@ -2060,6 +2073,10 @@ iconv-lite@0.4.13, iconv-lite@~0.4.13:
version "0.4.13"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
iconv-lite@0.4.15:
version "0.4.15"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
icss-replace-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.0.2.tgz#cb0b6054eb3af6edc9ab1d62d01933e2d4c8bfa5"
@ -3935,6 +3952,14 @@ range-parser@^1.0.3, range-parser@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
raw-body@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96"
dependencies:
bytes "2.4.0"
iconv-lite "0.4.15"
unpipe "1.0.0"
rc@^1.0.1, rc@^1.1.6, rc@~1.1.6:
version "1.1.7"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.7.tgz#c5ea564bb07aff9fd3a5b32e906c1d3a65940fea"
@ -5039,7 +5064,7 @@ unique-string@^1.0.0:
dependencies:
crypto-random-string "^1.0.0"
unpipe@~1.0.0:
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"