Merge pull request #247 from CatalystCode/ibex-version-1.0

Ibex version 1.0
This commit is contained in:
Mor Shemesh 2017-07-30 10:08:49 +03:00 коммит произвёл GitHub
Родитель 1f09310236 02ba6f83d2
Коммит a2222b503a
9 изменённых файлов: 304 добавлений и 251 удалений

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

@ -1,6 +1,6 @@
{ {
"main.css": "static/css/main.43e63d1e.css", "main.css": "static/css/main.43e63d1e.css",
"main.css.map": "static/css/main.43e63d1e.css.map", "main.css.map": "static/css/main.43e63d1e.css.map",
"main.js": "static/js/main.7f2eae21.js", "main.js": "static/js/main.203f58e7.js",
"main.js.map": "static/js/main.7f2eae21.js.map" "main.js.map": "static/js/main.203f58e7.js.map"
} }

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

@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="shortcut icon" href="/favicon.ico"><link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"/><title>React App</title><link href="/static/css/main.43e63d1e.css" rel="stylesheet"></head><body><div id="root"></div><script type="text/javascript" src="/static/js/main.7f2eae21.js"></script></body></html> <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="shortcut icon" href="/favicon.ico"><link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"/><title>React App</title><link href="/static/css/main.43e63d1e.css" rel="stylesheet"></head><body><div id="root"></div><script type="text/javascript" src="/static/js/main.203f58e7.js"></script></body></html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -1,5 +1,6 @@
import alt, { AbstractActions } from '../alt'; import alt, { AbstractActions } from '../alt';
import * as request from 'xhr-request'; import * as request from 'xhr-request';
import utils from '../utils';
interface IConfigurationsActions { interface IConfigurationsActions {
loadConfiguration(): any; loadConfiguration(): any;
@ -85,7 +86,7 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
createDashboard(dashboard: IDashboardConfig) { createDashboard(dashboard: IDashboardConfig) {
return (dispatcher: (dashboard: IDashboardConfig) => void) => { return (dispatcher: (dashboard: IDashboardConfig) => void) => {
let script = this.objectToString(dashboard); let script = utils.objectToString(dashboard);
request('/api/dashboards/' + dashboard.id, { request('/api/dashboards/' + dashboard.id, {
method: 'PUT', method: 'PUT',
json: true, json: true,
@ -122,7 +123,7 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
saveAsTemplate(template: IDashboardConfig) { saveAsTemplate(template: IDashboardConfig) {
return (dispatcher: (result: { template: IDashboardConfig }) => void) => { return (dispatcher: (result: { template: IDashboardConfig }) => void) => {
let script = this.objectToString(template); let script = utils.objectToString(template);
script = '/// <reference path="../../../client/@types/types.d.ts"/>\n' + script = '/// <reference path="../../../client/@types/types.d.ts"/>\n' +
'import * as _ from \'lodash\';\n\n' + 'import * as _ from \'lodash\';\n\n' +
@ -149,7 +150,7 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
saveConfiguration(dashboard: IDashboardConfig) { saveConfiguration(dashboard: IDashboardConfig) {
return (dispatcher: (dashboard: IDashboardConfig) => void) => { return (dispatcher: (dashboard: IDashboardConfig) => void) => {
let stringDashboard = this.objectToString(dashboard); let stringDashboard = utils.objectToString(dashboard);
request('/api/dashboards/' + dashboard.id, { request('/api/dashboards/' + dashboard.id, {
method: 'POST', method: 'POST',
@ -171,10 +172,6 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
}; };
} }
convertDashboardToString(dashboard: IDashboardConfig) {
return this.objectToString(dashboard);
}
failure(error: any) { failure(error: any) {
return { error }; return { error };
} }
@ -196,6 +193,10 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
}; };
} }
convertDashboardToString(dashboard: IDashboardConfig) {
return utils.convertDashboardToString(dashboard);
}
private getScript(source: string, callback?: () => void): boolean { private getScript(source: string, callback?: () => void): boolean {
let script: any = document.createElement('script'); let script: any = document.createElement('script');
let prior = document.getElementsByTagName('script')[0]; let prior = document.getElementsByTagName('script')[0];
@ -219,141 +220,6 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
script.src = source; script.src = source;
return true; return true;
} }
/**
* 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': {
if (obj === null) { return result = 'null'; }
// Iterating through all values in object
let objectValue = '';
let objectValues = [];
let valuesStringLength = 0;
Object.keys(obj).forEach((key: string, idx: number) => {
let value = this.objectToString(obj[key], indent + 1, true);
// if key contains '.' or '-'
let skey = key.search(/\.|\-/g) >= 0 ? `"${key}"` : `${key}`;
let mapping = `${skey}: ${value}`;
valuesStringLength += mapping.length;
objectValues.push(mapping);
});
if (valuesStringLength <= 120) {
result += `{ ${objectValues.join()} }`;
} else {
result += `{\n${sind}\t${objectValues.join(',\n' + sind + '\t')}\n${sind}}`;
}
break;
}
case 'string':
let stringValue = obj.toString();
let htmlString = stringValue.replace(/^\s+|\s+$/g, ''); // trim any leading and trailing whitespace
if ( htmlString.startsWith('<') && htmlString.endsWith('>') ) {
result += '`' + htmlString + '`'; // html needs to be wrapped in back ticks
} else {
stringValue = stringValue.replace(/\"/g, '\\"');
result += `"${stringValue}"`;
}
break;
case 'function': {
result += obj.toString();
break;
}
case 'number':
case 'boolean': {
result += `${obj}`;
break;
}
case 'array': {
let arrayStringLength = 0;
let mappedValues = (obj as any[]).map(value => {
let res = this.objectToString(value, indent + 1, true);
arrayStringLength += res.length;
return res;
});
if (arrayStringLength <= 120) {
result += `[${mappedValues.join()}]`;
} else {
result += `[\n${sind}\t${mappedValues.join(',\n' + sind + '\t')}\n${sind}]`;
}
break;
}
case 'undefined': {
result += `undefined`;
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') {
global['eval']('obj[i] = ' + parsedString[i] );
} 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);
global['eval']('dataSource.calculated = ' + calculated);
}
});
}
} }
const configurationsActions = alt.createActions<IConfigurationsActions>(ConfigurationsActions); const configurationsActions = alt.createActions<IConfigurationsActions>(ConfigurationsActions);

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

@ -201,6 +201,10 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
template.description = this.state.newTemplateDescription; template.description = this.state.newTemplateDescription;
template.category = 'Custom Templates'; template.category = 'Custom Templates';
template.id = template.url = dashboard.id + (Math.floor(Math.random() * 1000) + 1); // generate random id template.id = template.url = dashboard.id + (Math.floor(Math.random() * 1000) + 1); // generate random id
// Removing connections so private info will not be included
template.config.connections = {};
ConfigurationsActions.saveAsTemplate(template); ConfigurationsActions.saveAsTemplate(template);
window.location.href = '/'; window.location.href = '/';
this.setState({ askSaveAsTemplate: false }); this.setState({ askSaveAsTemplate: false });

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

@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import Toolbar from 'react-md/lib/Toolbars';
import Button from 'react-md/lib/Buttons/Button'; import Button from 'react-md/lib/Buttons/Button';
import CircularProgress from 'react-md/lib/Progress/CircularProgress'; import CircularProgress from 'react-md/lib/Progress/CircularProgress';
import { Card, CardTitle, CardActions, CardText } from 'react-md/lib/Cards'; import { Card, CardTitle, CardActions, CardText } from 'react-md/lib/Cards';
@ -13,9 +14,13 @@ import SetupStore from '../../stores/SetupStore';
import ConfigurationStore from '../../stores/ConfigurationsStore'; import ConfigurationStore from '../../stores/ConfigurationsStore';
import ConfigurationsActions from '../../actions/ConfigurationsActions'; import ConfigurationsActions from '../../actions/ConfigurationsActions';
import utils from '../../utils';
import IDownloadFile, { exportDataSources, createDownloadFiles, downloadBlob } from '../Dashboard/DownloadFile';
const renderHTML = require('react-render-html'); const renderHTML = require('react-render-html');
const MultipleSpacesRegex = / +/g;
const styles = { const styles = {
card: { card: {
minWidth: 400, minWidth: 400,
@ -97,6 +102,8 @@ export default class Home extends React.Component<any, IHomeState> {
this.onLoad = this.onLoad.bind(this); this.onLoad = this.onLoad.bind(this);
this.setFile = this.setFile.bind(this); this.setFile = this.setFile.bind(this);
this.updateFileName = this.updateFileName.bind(this); this.updateFileName = this.updateFileName.bind(this);
this.onExportTemplate = this.onExportTemplate.bind(this);
this.downloadTemplate = this.downloadTemplate.bind(this);
} }
updateConfiguration(state: {templates: IDashboardConfig[], template: IDashboardConfig, creationState: string}) { updateConfiguration(state: {templates: IDashboardConfig[], template: IDashboardConfig, creationState: string}) {
@ -105,6 +112,9 @@ export default class Home extends React.Component<any, IHomeState> {
template: state.template, template: state.template,
creationState: state.creationState creationState: state.creationState
}); });
if (this.state.stage === 'requestDownloadTemplate') {
this.downloadTemplate(this.state.template);
}
} }
updateSetup(state: IHomeState) { updateSetup(state: IHomeState) {
@ -211,6 +221,19 @@ export default class Home extends React.Component<any, IHomeState> {
this.setState({ importedFileContent }); this.setState({ importedFileContent });
} }
onExportTemplate(templateId: string) {
this.setState({ stage: 'requestDownloadTemplate' });
ConfigurationsActions.loadTemplate(templateId);
}
downloadTemplate(template: IDashboardConfig) {
template.layouts = template.layouts || {};
let stringDashboard = utils.convertDashboardToString(template);
var dashboardName = template.id.replace(MultipleSpacesRegex, ' ');
dashboardName = template.id.replace(MultipleSpacesRegex, '_');
downloadBlob('return ' + stringDashboard, 'application/json', dashboardName + '.private.ts');
}
render() { render() {
let { loaded, redirectUrl, templates, selectedTemplateId, template } = this.state; let { loaded, redirectUrl, templates, selectedTemplateId, template } = this.state;
let { importVisible } = this.state; let { importVisible } = this.state;
@ -241,6 +264,14 @@ export default class Home extends React.Component<any, IHomeState> {
</MediaOverlay> </MediaOverlay>
</Media> </Media>
<CardActions style={styles.fabs}> <CardActions style={styles.fabs}>
<Button
floating
secondary
style={{ backgroundColor: '#959ba5', marginRight: '2px' }}
onClick={this.onExportTemplate.bind(this, tmpl.id)}
>
file_download
</Button>
<Button <Button
floating floating
secondary secondary
@ -268,17 +299,37 @@ export default class Home extends React.Component<any, IHomeState> {
categories[category].push(createCard(tmpl, index)); categories[category].push(createCard(tmpl, index));
}); });
return ( let toolbarActions = [];
<div> toolbarActions.push(
<div style={{ textAlign: 'right' }}> (
<Button <Button
flat flat
tooltipLabel="Import dashboard" tooltipLabel="Import dashboard"
onClick={this.onOpenImport.bind(this)} onClick={this.onOpenImport.bind(this)}
label="Import dashboard" label="Import dashboard"
>file_upload >file_upload
</Button> </Button>
<Dialog )
);
return (
<div className="md-cell md-cell--12">
<Toolbar actions={toolbarActions} />
{
Object.keys(categories).map((category, index) => {
if (!categories[category].length) { return null; }
return (
<div key={index}>
<h1>{category}</h1>
<div className="md-grid">
{categories[category]}
</div>
</div>
);
})
}
<Dialog
id="ImportDashboard" id="ImportDashboard"
visible={importVisible || false} visible={importVisible || false}
title="Import dashboard" title="Import dashboard"
@ -304,22 +355,7 @@ export default class Home extends React.Component<any, IHomeState> {
lineDirection="center" lineDirection="center"
placeholder="Choose an ID for the imported dashboard" placeholder="Choose an ID for the imported dashboard"
/> />
</Dialog> </Dialog>
</div>
{
Object.keys(categories).map((category, index) => {
if (!categories[category].length) { return null; }
return (
<div key={index}>
<h1>{category}</h1>
<div className="md-grid">
{categories[category]}
</div>
</div>
);
})
}
<Dialog <Dialog
id="templateInfoDialog" id="templateInfoDialog"

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

@ -1,7 +1,7 @@
import * as moment from 'moment'; import * as moment from 'moment';
export default { export default class Utils {
kmNumber: (num: number): string => { static kmNumber(num: number): string {
if (isNaN(num)) { return ''; } if (isNaN(num)) { return ''; }
return ( return (
@ -11,9 +11,148 @@ export default {
(num / 1000).toFixed(1) + 'K' : (num / 1000).toFixed(1) + 'K' :
(num % 1 * 10) !== 0 ? (num % 1 * 10) !== 0 ?
num.toFixed(1).toString() : num.toString()); num.toFixed(1).toString() : num.toString());
}, }
ago: (date: Date): string => { static ago(date: Date): string {
return moment(date).fromNow(); return moment(date).fromNow();
} }
static convertDashboardToString(dashboard: IDashboardConfig) {
return Utils.objectToString(dashboard);
}
/**
* Convret a json object with functions to string
* @param obj an object with functions to convert to string
*/
static 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': {
if (obj === null) { return result = 'null'; }
// Iterating through all values in object
let objectValue = '';
let objectValues = [];
let valuesStringLength = 0;
Object.keys(obj).forEach((key: string, idx: number) => {
let value = Utils.objectToString(obj[key], indent + 1, true);
// if key contains '.' or '-'
let skey = key.search(/\.|\-/g) >= 0 ? `"${key}"` : `${key}`;
let mapping = `${skey}: ${value}`;
valuesStringLength += mapping.length;
objectValues.push(mapping);
});
if (valuesStringLength <= 120) {
result += `{ ${objectValues.join()} }`;
} else {
result += `{\n${sind}\t${objectValues.join(',\n' + sind + '\t')}\n${sind}}`;
}
break;
}
case 'string':
let stringValue = obj.toString();
let htmlString = stringValue.replace(/^\s+|\s+$/g, ''); // trim any leading and trailing whitespace
if ( htmlString.startsWith('<') && htmlString.endsWith('>') ) {
result += '`' + htmlString + '`'; // html needs to be wrapped in back ticks
} else {
stringValue = stringValue.replace(/\"/g, '\\"');
result += `"${stringValue}"`;
}
break;
case 'function': {
result += obj.toString();
break;
}
case 'number':
case 'boolean': {
result += `${obj}`;
break;
}
case 'array': {
let arrayStringLength = 0;
let mappedValues = (obj as any[]).map(value => {
let res = Utils.objectToString(value, indent + 1, true);
arrayStringLength += res.length;
return res;
});
if (arrayStringLength <= 120) {
result += `[${mappedValues.join()}]`;
} else {
result += `[\n${sind}\t${mappedValues.join(',\n' + sind + '\t')}\n${sind}]`;
}
break;
}
case 'undefined': {
result += `undefined`;
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
*/
static 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') {
global['eval']('obj[i] = ' + parsedString[i] );
} else {
obj[i] = parsedString[i];
}
} else if (typeof parsedString[i] === 'object') {
obj[i] = Utils.stringToObject(parsedString[i]);
}
}
return obj;
}
private static 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);
global['eval']('dataSource.calculated = ' + calculated);
}
});
}
}; };

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

@ -80,7 +80,7 @@ router.get('/dashboards', (req, res) => {
const jsonDefinition = getMetadata(fileContents); const jsonDefinition = getMetadata(fileContents);
let content = 'return ' + JSON.stringify(jsonDefinition); let content = 'return ' + JSON.stringify(jsonDefinition);
// Ensuing this dashboard is loaded into the dashboards array on the page // Ensuring this dashboard is loaded into the dashboards array on the page
script += ` script += `
(function (window) { (function (window) {
var dashboard = (function () { var dashboard = (function () {
@ -229,8 +229,12 @@ router.get('/templates/:id', (req, res) => {
}); });
router.put('/templates/:id', (req, res) => { router.put('/templates/:id', (req, res) => {
let { id } = req.params; let { id } = req.params || {};
let { script } = req.body || ''; let { script } = req.body || {};
if (!id || !script) {
return res.end({ error: 'No id or scripts were supplied for saving the template' });
}
const { privateTemplate } = paths(); const { privateTemplate } = paths();
@ -255,8 +259,12 @@ router.put('/templates/:id', (req, res) => {
}); });
router.put('/dashboards/:id', (req, res) => { router.put('/dashboards/:id', (req, res) => {
let { id } = req.params; let { id } = req.params || {};
let { script } = req.body || ''; let { script } = req.body || {};
if (!id || !script) {
return res.end({ error: 'No id or script were supplied for the new dashboard' });
}
const { privateDashboard } = paths(); const { privateDashboard } = paths();
let dashboardPath = path.join(privateDashboard, id + '.private.js'); let dashboardPath = path.join(privateDashboard, id + '.private.js');