Merge pull request #246 from CatalystCode/ibex-version-1.0
Ibex version 1.0
This commit is contained in:
Коммит
1f09310236
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"main.css": "static/css/main.43e63d1e.css",
|
||||
"main.css.map": "static/css/main.43e63d1e.css.map",
|
||||
"main.js": "static/js/main.d2afa0dc.js",
|
||||
"main.js.map": "static/js/main.d2afa0dc.js.map"
|
||||
"main.js": "static/js/main.7f2eae21.js",
|
||||
"main.js.map": "static/js/main.7f2eae21.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.d2afa0dc.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.7f2eae21.js"></script></body></html>
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -11,6 +11,7 @@ interface IConfigurationsActions {
|
|||
submitDashboardFile(content: string, fileName: string): void;
|
||||
convertDashboardToString(dashboard: IDashboardConfig): string;
|
||||
deleteDashboard(id: string): any;
|
||||
saveAsTemplate(template: IDashboardConfig): any;
|
||||
}
|
||||
|
||||
class ConfigurationsActions extends AbstractActions implements IConfigurationsActions {
|
||||
|
@ -18,15 +19,15 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
|
|||
super(alt);
|
||||
}
|
||||
|
||||
submitDashboardFile = (content, dashboardId) => {
|
||||
submitDashboardFile(content: string, dashboardId: string) {
|
||||
return (dispatcher: (json: any) => void) => {
|
||||
|
||||
// Replace both 'id' and 'url' with the requested id from the user
|
||||
var idRegExPattern = /id: \".*\",/i;
|
||||
var urlRegExPatternt = /url: \".*\",/i;
|
||||
var updatedContent =
|
||||
const idRegExPattern = /id: \".*\",/i;
|
||||
const urlRegExPatternt = /url: \".*\",/i;
|
||||
const updatedContent =
|
||||
content.replace(idRegExPattern, 'id: \"' + dashboardId + '\",')
|
||||
.replace(urlRegExPatternt, 'url: \"' + dashboardId + '\",');
|
||||
.replace(urlRegExPatternt, 'url: \"' + dashboardId + '\",');
|
||||
|
||||
request(
|
||||
'/api/dashboards/' + dashboardId,
|
||||
|
@ -39,6 +40,7 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
|
|||
if (error || (json && json.errors)) {
|
||||
return this.failure(error || json.errors);
|
||||
}
|
||||
|
||||
// redirect to the newly imported dashboard
|
||||
window.location.replace('dashboard/' + dashboardId);
|
||||
return dispatcher(json);
|
||||
|
@ -117,6 +119,33 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
|
|||
};
|
||||
}
|
||||
|
||||
saveAsTemplate(template: IDashboardConfig) {
|
||||
|
||||
return (dispatcher: (result: { template: IDashboardConfig }) => void) => {
|
||||
let script = this.objectToString(template);
|
||||
|
||||
script = '/// <reference path="../../../client/@types/types.d.ts"/>\n' +
|
||||
'import * as _ from \'lodash\';\n\n' +
|
||||
'export const config: IDashboardConfig = /*return*/ ' + script;
|
||||
return request(
|
||||
'/api/templates/' + template.id,
|
||||
{
|
||||
method: 'PUT',
|
||||
json: true,
|
||||
body: { script: script }
|
||||
},
|
||||
(error: any, json: any) => {
|
||||
|
||||
if (error || (json && json.errors)) {
|
||||
return this.failure(error || json.errors);
|
||||
}
|
||||
|
||||
return dispatcher(json);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
saveConfiguration(dashboard: IDashboardConfig) {
|
||||
return (dispatcher: (dashboard: IDashboardConfig) => void) => {
|
||||
|
||||
|
@ -299,7 +328,7 @@ class ConfigurationsActions extends AbstractActions implements IConfigurationsAc
|
|||
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 */
|
||||
global['eval']('obj[i] = ' + parsedString[i] );
|
||||
|
||||
} else {
|
||||
obj[i] = parsedString[i];
|
||||
|
@ -321,7 +350,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 */
|
||||
global['eval']('dataSource.calculated = ' + calculated);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import FontIcon from 'react-md/lib/FontIcons';
|
|||
import Avatar from 'react-md/lib/Avatars';
|
||||
import Subheader from 'react-md/lib/Subheaders';
|
||||
import Divider from 'react-md/lib/Dividers';
|
||||
import TextField from 'react-md/lib/TextFields';
|
||||
|
||||
interface IDashboardProps {
|
||||
dashboard?: IDashboardConfig;
|
||||
|
@ -41,6 +42,7 @@ interface IDashboardState {
|
|||
editMode?: boolean;
|
||||
askDelete?: boolean;
|
||||
askDownload?: boolean;
|
||||
askSaveAsTemplate?: boolean;
|
||||
downloadFiles?: IDownloadFile[];
|
||||
downloadFormat?: string;
|
||||
mounted?: boolean;
|
||||
|
@ -51,15 +53,18 @@ interface IDashboardState {
|
|||
visibilityFlags?: IDict<boolean>;
|
||||
infoVisible?: boolean;
|
||||
infoHtml?: string;
|
||||
newTemplateName?: string;
|
||||
newTemplateDescription?: string;
|
||||
}
|
||||
|
||||
export default class Dashboard extends React.Component<IDashboardProps, IDashboardState> {
|
||||
|
||||
|
||||
layouts = {};
|
||||
|
||||
state = {
|
||||
editMode: false,
|
||||
askDelete: false,
|
||||
askSaveAsTemplate: false,
|
||||
askDownload: false,
|
||||
downloadFiles: [],
|
||||
downloadFormat: 'json',
|
||||
|
@ -71,6 +76,8 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
visibilityFlags: {},
|
||||
infoVisible: false,
|
||||
infoHtml: '',
|
||||
newTemplateName: '',
|
||||
newTemplateDescription: ''
|
||||
};
|
||||
|
||||
constructor(props: IDashboardProps) {
|
||||
|
@ -91,10 +98,17 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
this.onClickDownloadFile = this.onClickDownloadFile.bind(this);
|
||||
this.onChangeDownloadFormat = this.onChangeDownloadFormat.bind(this);
|
||||
this.onDownloadDashboard = this.onDownloadDashboard.bind(this);
|
||||
this.onSaveAsTemplate = this.onSaveAsTemplate.bind(this);
|
||||
this.newTemplateNameChange = this.newTemplateNameChange.bind(this);
|
||||
this.onSaveAsTemplateApprove = this.onSaveAsTemplateApprove.bind(this);
|
||||
this.onSaveAsTemplateCancel = this.onSaveAsTemplateCancel.bind(this);
|
||||
this.newTemplateDescriptionChange = this.newTemplateDescriptionChange.bind(this);
|
||||
|
||||
VisibilityStore.listen(state => {
|
||||
this.setState({ visibilityFlags: state.flags });
|
||||
});
|
||||
this.state.newTemplateName = this.props.dashboard.name;
|
||||
this.state.newTemplateDescription = this.props.dashboard.description;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -176,6 +190,34 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
this.setState({ askDelete: true });
|
||||
}
|
||||
|
||||
onSaveAsTemplate() {
|
||||
this.setState({ askSaveAsTemplate: true });
|
||||
}
|
||||
|
||||
onSaveAsTemplateApprove() {
|
||||
let { dashboard } = this.props;
|
||||
var template = _.cloneDeep(dashboard);
|
||||
template.name = this.state.newTemplateName;
|
||||
template.description = this.state.newTemplateDescription;
|
||||
template.category = 'Custom Templates';
|
||||
template.id = template.url = dashboard.id + (Math.floor(Math.random() * 1000) + 1); // generate random id
|
||||
ConfigurationsActions.saveAsTemplate(template);
|
||||
window.location.href = '/';
|
||||
this.setState({ askSaveAsTemplate: false });
|
||||
}
|
||||
|
||||
onSaveAsTemplateCancel() {
|
||||
this.setState({ askSaveAsTemplate: false });
|
||||
}
|
||||
|
||||
newTemplateNameChange(value: string, e: any) {
|
||||
this.setState({ newTemplateName: value });
|
||||
}
|
||||
|
||||
newTemplateDescriptionChange(value: string, e: any) {
|
||||
this.setState({ newTemplateDescription: value });
|
||||
}
|
||||
|
||||
onDeleteDashboardApprove() {
|
||||
let { dashboard } = this.props;
|
||||
if (!dashboard) {
|
||||
|
@ -252,7 +294,10 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
askDownload,
|
||||
downloadFiles,
|
||||
downloadFormat,
|
||||
askConfig
|
||||
askConfig ,
|
||||
askSaveAsTemplate,
|
||||
newTemplateName,
|
||||
newTemplateDescription
|
||||
} = this.state;
|
||||
const { infoVisible, infoHtml } = this.state;
|
||||
const layout = this.state.layouts[currentBreakpoint];
|
||||
|
@ -317,6 +362,12 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
<span>
|
||||
<Button key="delete" icon tooltipLabel="Delete dashboard" onClick={this.onDeleteDashboard}>delete</Button>
|
||||
</span>
|
||||
),
|
||||
(
|
||||
<span>
|
||||
<Button key="saveAsTemplate" icon tooltipLabel="Save as template"
|
||||
onClick={this.onSaveAsTemplate}>cloud_download</Button>
|
||||
</span>
|
||||
)
|
||||
);
|
||||
toolbarActions.reverse();
|
||||
|
@ -451,6 +502,38 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
Are you sure you want to permanently delete this dashboard?
|
||||
</p>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
dialogStyle={{ width: '50%' }}
|
||||
id="saveAsTemplateDialog"
|
||||
visible={askSaveAsTemplate}
|
||||
title="Save this dashoard as a custom template"
|
||||
modal
|
||||
actions={[
|
||||
{ onClick: this.onSaveAsTemplateApprove, primary: false, label: 'Save as custom template', },
|
||||
{ onClick: this.onSaveAsTemplateCancel, primary: true, label: 'Cancel' }
|
||||
]}
|
||||
>
|
||||
<p>You can save this dashboard as a custom template for a future reuse</p>
|
||||
<TextField
|
||||
id="templateName"
|
||||
label="New Template Name"
|
||||
placeholder="Template Name"
|
||||
className="md-cell md-cell--bottom"
|
||||
value={newTemplateName}
|
||||
onChange={this.newTemplateNameChange}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
id="templateDescription"
|
||||
label="New Template Description"
|
||||
placeholder="Template Description"
|
||||
className="md-cell md-cell--bottom"
|
||||
value={newTemplateDescription}
|
||||
onChange={this.newTemplateDescriptionChange}
|
||||
required
|
||||
/>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -272,6 +272,7 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
<div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Button
|
||||
flat
|
||||
tooltipLabel="Import dashboard"
|
||||
onClick={this.onOpenImport.bind(this)}
|
||||
label="Import dashboard"
|
||||
|
@ -279,7 +280,7 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
</Button>
|
||||
<Dialog
|
||||
id="ImportDashboard"
|
||||
visible={importVisible}
|
||||
visible={importVisible || false}
|
||||
title="Import dashboard"
|
||||
modal
|
||||
actions={[
|
||||
|
@ -297,7 +298,7 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
<TextField
|
||||
id="dashboardFileName"
|
||||
label="Dashboard ID"
|
||||
value={fileName}
|
||||
value={fileName || ''}
|
||||
onChange={this.updateFileName}
|
||||
disabled={!importedFileContent}
|
||||
lineDirection="center"
|
||||
|
@ -307,10 +308,10 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
</div>
|
||||
|
||||
{
|
||||
Object.keys(categories).map(category => {
|
||||
Object.keys(categories).map((category, index) => {
|
||||
if (!categories[category].length) { return null; }
|
||||
return (
|
||||
<div>
|
||||
<div key={index}>
|
||||
<h1>{category}</h1>
|
||||
<div className="md-grid">
|
||||
{categories[category]}
|
||||
|
@ -323,7 +324,7 @@ export default class Home extends React.Component<any, IHomeState> {
|
|||
<Dialog
|
||||
id="templateInfoDialog"
|
||||
title={infoTitle}
|
||||
visible={infoVisible}
|
||||
visible={infoVisible || false}
|
||||
onHide={this.onCloseInfo}
|
||||
dialogStyle={{ width: '80%' }}
|
||||
contentStyle={{ padding: '0', maxHeight: 'calc(100vh - 148px)' }}
|
||||
|
|
|
@ -105,7 +105,10 @@ export default class MapData extends GenericComponent<IMapDataProps, IMapDataSta
|
|||
promises.push(promise);
|
||||
promise.then(results => {
|
||||
let markupPopup = (popup && L.popup().setContent(popup)) || null;
|
||||
markers.push({ lat: results[0].y, lng: results[0].x, popup: markupPopup });
|
||||
|
||||
if (results.length) {
|
||||
markers.push({ lat: results[0].y, lng: results[0].x, popup: markupPopup });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -42,7 +42,8 @@ const getMetadata = (text) => {
|
|||
|
||||
const paths = () => ({
|
||||
privateDashboard: path.join(__dirname, '..', 'dashboards'),
|
||||
preconfDashboard: path.join(__dirname, '..', 'dashboards', 'preconfigured')
|
||||
preconfDashboard: path.join(__dirname, '..', 'dashboards', 'preconfigured'),
|
||||
privateTemplate: path.join(__dirname, '..', 'dashboards', 'customTemplates')
|
||||
});
|
||||
|
||||
const isValidFile = (filePath) => {
|
||||
|
@ -57,10 +58,18 @@ const getFileContents = (filePath) => {
|
|||
: contents;
|
||||
}
|
||||
|
||||
const ensureCustomTemplatesFolderExists = () => {
|
||||
const { privateTemplate } = paths();
|
||||
|
||||
if (!fs.existsSync(privateTemplate)) {
|
||||
fs.mkdirSync(privateTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/dashboards', (req, res) => {
|
||||
|
||||
const { privateDashboard, preconfDashboard } = paths();
|
||||
|
||||
const { privateDashboard, preconfDashboard, privateTemplate } = paths();
|
||||
|
||||
let script = '';
|
||||
let files = fs.readdirSync(privateDashboard);
|
||||
if (files && files.length) {
|
||||
|
@ -85,6 +94,7 @@ router.get('/dashboards', (req, res) => {
|
|||
});
|
||||
}
|
||||
|
||||
// read preconfigured templates and custom templates
|
||||
let templates = fs.readdirSync(preconfDashboard);
|
||||
if (templates && templates.length) {
|
||||
templates.forEach((fileName) => {
|
||||
|
@ -107,6 +117,30 @@ router.get('/dashboards', (req, res) => {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
ensureCustomTemplatesFolderExists();
|
||||
let customTemplates = fs.readdirSync(privateTemplate);
|
||||
if (customTemplates && customTemplates.length) {
|
||||
customTemplates.forEach((fileName) => {
|
||||
let filePath = path.join(privateTemplate, fileName);
|
||||
if (isValidFile(filePath)) {
|
||||
const fileContents = getFileContents(filePath);
|
||||
const jsonDefinition = getMetadata(fileContents);
|
||||
let content = 'return ' + JSON.stringify(jsonDefinition);
|
||||
|
||||
// Ensuing this dashboard is loaded into the dashboards array on the page
|
||||
script += `
|
||||
(function (window) {
|
||||
var dashboardTemplate = (function () {
|
||||
${content}
|
||||
})();
|
||||
window.dashboardTemplates = window.dashboardTemplates || [];
|
||||
window.dashboardTemplates.push(dashboardTemplate);
|
||||
})(window);
|
||||
`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.send(script);
|
||||
});
|
||||
|
@ -163,13 +197,19 @@ router.post('/dashboards/:id', (req, res) => {
|
|||
router.get('/templates/:id', (req, res) => {
|
||||
|
||||
let templateId = req.params.id;
|
||||
let preconfDashboard = path.join(__dirname, '..', 'dashboards', 'preconfigured');
|
||||
let templatePath = path.join(__dirname, '..', 'dashboards', 'preconfigured');
|
||||
|
||||
let script = '';
|
||||
let dashboardFile = getFileById(preconfDashboard, templateId);
|
||||
|
||||
if (dashboardFile) {
|
||||
let filePath = path.join(preconfDashboard, dashboardFile);
|
||||
|
||||
let templateFile = getFileById(templatePath, templateId);
|
||||
|
||||
if (!templateFile) {
|
||||
//fallback to custom template
|
||||
templatePath = path.join(__dirname, '..', 'dashboards', 'customTemplates');
|
||||
templateFile = getFileById(templatePath, templateId);
|
||||
}
|
||||
if (templateFile) {
|
||||
let filePath = path.join(templatePath, templateFile);
|
||||
if (isValidFile(filePath)) {
|
||||
const content = getFileContents(filePath);
|
||||
|
||||
|
@ -188,6 +228,32 @@ router.get('/templates/:id', (req, res) => {
|
|||
res.send(script);
|
||||
});
|
||||
|
||||
router.put('/templates/:id', (req, res) => {
|
||||
let { id } = req.params;
|
||||
let { script } = req.body || '';
|
||||
|
||||
const { privateTemplate } = paths();
|
||||
|
||||
ensureCustomTemplatesFolderExists();
|
||||
|
||||
let templatePath = path.join(privateTemplate, id + '.private.ts');
|
||||
let templateFile = getFileById(privateTemplate, id);
|
||||
let exists = fs.existsSync(templatePath);
|
||||
|
||||
if (templateFile || exists) {
|
||||
return res.json({ errors: ['Dashboard id or filename already exists'] });
|
||||
}
|
||||
|
||||
fs.writeFile(templatePath, script, err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.end(err);
|
||||
}
|
||||
|
||||
res.json({ script });
|
||||
});
|
||||
});
|
||||
|
||||
router.put('/dashboards/:id', (req, res) => {
|
||||
let { id } = req.params;
|
||||
let { script } = req.body || '';
|
||||
|
|
Загрузка…
Ссылка в новой задаче