+ this.fetchDatacache(
+ JSON.stringify(this.state.plotlyJSON),
+ 'plot'
+ )}
+ >
+ Send chart to Chart Studio
+
this.fetchDatacache(this.getCSVString(), 'grid')}
@@ -465,19 +474,12 @@ class Preview extends Component {
{!isOnPrem() &&
this.fetchDatacache(this.getCSVString(), 'csv')}
+ onClick={() => window.open(
+ `data:text/csv;base64,${Buffer.from(this.getCSVString()).toString('base64')}`
+ )}
>
Download CSV
}
- this.fetchDatacache(
- JSON.stringify(this.state.plotlyJSON),
- 'plot'
- )}
- >
- Send chart to Chart Studio
-
{link.url}
}
- {link.type === 'csv' &&
-
-
💾 Your CSV has been saved ⬇️
-
{link.url}
-
- }
{link.type === 'plot' &&
📈 Link to your chart on Chart Studio ⬇️
diff --git a/app/components/Settings/Settings.react.js b/app/components/Settings/Settings.react.js
index b87c995e5..11d272546 100644
--- a/app/components/Settings/Settings.react.js
+++ b/app/components/Settings/Settings.react.js
@@ -60,10 +60,14 @@ class Settings extends Component {
this.intervals.checkHTTPSEndpointInterval = setInterval(() => {
if (this.state.urls.https) {
- fetch(this.state.urls.https).then(() => {
+ fetch(this.state.urls.https)
+ .then(() => {
this.setState({httpsServerIsOK: true});
clearInterval(this.intervals.checkHTTPSEndpointInterval);
clearInterval(this.intervals.timeElapsedInterval);
+ })
+ .catch(() => {
+ // silence fetch errors
});
}
}, 5 * 1000);
@@ -247,6 +251,7 @@ class Settings extends Component {
apacheDrillStorageRequest,
apacheDrillS3KeysRequest,
connections,
+ connectRequest,
deleteTab,
elasticsearchMappingsRequest,
getSqlSchema,
@@ -283,6 +288,11 @@ class Settings extends Component {
const dialect = connections[selectedTab].dialect;
+ const queryPanelDisabled = (
+ connectRequest.status !== 200 ||
+ (!selectedTable && contains(dialect, SQL_DIALECTS_USING_EDITOR))
+ );
+
return (
Connection
- {this.props.connectRequest.status === 200 && selectedTable ? (
- Query
- ) : (
- Query
- )}
+ Query
{isOnPrem() ||
@@ -335,7 +341,11 @@ class Settings extends Component {
- {this.props.connectRequest.status === 200 && selectedTable ? (
+ {queryPanelDisabled ? (
+
+
Please connect to a data store in the Connection tab first.
+
+ ) : (
- ) : (
-
-
Please connect to a data store in the Connection tab first.
-
)}
diff --git a/app/components/Settings/UserConnections/UserConnections.react.js b/app/components/Settings/UserConnections/UserConnections.react.js
index 04058fdb1..13b1cae5e 100644
--- a/app/components/Settings/UserConnections/UserConnections.react.js
+++ b/app/components/Settings/UserConnections/UserConnections.react.js
@@ -5,21 +5,12 @@ import Filedrop from './filedrop.jsx';
import {contains} from 'ramda';
import {CONNECTION_CONFIG, SAMPLE_DBS} from '../../../constants/constants';
-import {dynamicRequireElectron} from '../../../utils/utils';
-
-let dialog;
-try {
- dialog = dynamicRequireElectron().remote.dialog;
-} catch (e) {
- dialog = null;
-}
+import {showOpenDialog} from '../../../utils/utils';
/*
* Displays and alters user inputs for `configuration`
* username, password, and local port number.
*/
-
-
export default class UserConnections extends Component {
constructor(props) {
super(props);
@@ -47,7 +38,7 @@ export default class UserConnections extends Component {
getStorageOnClick(setting) {
// sqlite requires a path
return () => {
- dialog.showOpenDialog({
+ showOpenDialog({
properties: ['openFile'],
filters: [{
name: 'databases',
diff --git a/app/utils/utils.js b/app/utils/utils.js
index 5b030271e..11b49e1f1 100644
--- a/app/utils/utils.js
+++ b/app/utils/utils.js
@@ -1,7 +1,20 @@
import {contains} from 'ramda';
-export function dynamicRequireElectron() {
- return window.require('electron');
+export function isElectron() {
+ // the electron's main process preloads a script that sets up `window.$falcon`
+ return Boolean(window.$falcon);
+}
+
+export function setUsernameListener(callback) {
+ if (isElectron()) {
+ window.$falcon.setUsernameListener(callback);
+ }
+}
+
+export function showOpenDialog(options, callback) {
+ if (isElectron()) {
+ return window.$falcon.showOpenDialog(options, callback);
+ }
}
export function baseUrl() {
@@ -40,10 +53,6 @@ export function plotlyUrl() {
return 'https://plot.ly';
}
-export function isElectron() {
- return window.process && window.process.type === 'renderer';
-}
-
export function homeUrl() {
return (isOnPrem()) ?
'/external-data-connector' :
diff --git a/backend/constants.js b/backend/constants.js
index 004b8d4a4..35d19a245 100644
--- a/backend/constants.js
+++ b/backend/constants.js
@@ -1,5 +1,11 @@
import {getSetting} from './settings';
+export function getUnsecuredCookieOptions() {
+ return {
+ path: getSetting('WEB_BASE_PATHNAME')
+ };
+}
+
export function getCookieOptions() {
return {
secure: getSetting('SSL_ENABLED'),
diff --git a/backend/main.development.js b/backend/main.development.js
index dbac65e34..90aa99ee2 100644
--- a/backend/main.development.js
+++ b/backend/main.development.js
@@ -1,8 +1,11 @@
-import {app, BrowserWindow} from 'electron';
+import {app, BrowserWindow, dialog, shell} from 'electron';
+import fs from 'fs';
+import path from 'path';
+
import {contains} from 'ramda';
-import Logger from './logger';
-import {setupMenus} from './menus';
+import Logger from './logger.js';
+import {setupMenus} from './menus.js';
import Servers from './routes.js';
Logger.log('Starting application', 2);
@@ -26,7 +29,10 @@ server.queryScheduler.loadQueries();
app.on('ready', () => {
let mainWindow = new BrowserWindow({
- show: true,
+ webPreferences: {
+ nodeIntegration: false,
+ preload: path.resolve(path.join(__dirname, 'preload.js'))
+ },
width: 1024,
height: 1024
});
@@ -62,7 +68,46 @@ app.on('ready', () => {
// prevent navigation out of HTTP_URL
// see https://electronjs.org/docs/api/web-contents#event-will-navigate
mainWindow.webContents.on('will-navigate', (event, url) => {
- if (!url.startsWith(HTTP_URL)) event.preventDefault();
+ if (!url.startsWith(HTTP_URL)) {
+ Logger.log(`Preventing navigation to ${url}`, 2);
+ event.preventDefault();
+ }
+ });
+
+ // prevent navigation out of HTTP_URL
+ // see https://electronjs.org/docs/api/web-contents#event-new-window
+ mainWindow.webContents.on('new-window', (event, url) => {
+ event.preventDefault();
+
+ if (!url.startsWith('data:')) {
+ Logger.log(`Opening ${url}`, 2);
+ shell.openExternal(url);
+ return;
+ }
+
+ // only download data URLs that contain a CSV file
+ if (!url.startsWith('data:text/csv;base64,')) {
+ Logger.log(`Preventing request ${url}`, 2);
+ return;
+ }
+
+ dialog.showSaveDialog({
+ title: 'Save CSV File',
+ filters: [{
+ name: 'CSV files',
+ extensions: ['csv']
+ }]
+ }, (filename) => {
+ if (!filename) {
+ // silence error thrown when user cancels the save dialog
+ return;
+ }
+
+ fs.writeFileSync(
+ filename,
+ Buffer.from(url.slice(1 + url.indexOf(',')), 'base64').toString()
+ );
+ });
});
mainWindow.on('closed', () => {
diff --git a/backend/plugins/authorization.js b/backend/plugins/authorization.js
index 1d6fcc82c..69eaf1761 100644
--- a/backend/plugins/authorization.js
+++ b/backend/plugins/authorization.js
@@ -1,9 +1,13 @@
+import fetch from 'node-fetch';
import {contains} from 'ramda';
+
+import {generateAndSaveAccessToken} from '../utils/authUtils.js';
+import {
+ getAccessTokenCookieOptions,
+ getUnsecuredCookieOptions
+} from '../constants.js';
+import Logger from '../logger.js';
import {getSetting} from '../settings.js';
-import {getAccessTokenCookieOptions} from '../constants.js';
-import {generateAndSaveAccessToken} from '../utils/authUtils';
-import Logger from '../logger';
-import fetch from 'node-fetch';
/*
* backend does not see `/external-data-connector` in on-prem (because it is proxied).
@@ -29,7 +33,14 @@ export function PlotlyOAuth(electron) {
return function isAuthorized(req, res, next) {
const path = req.href();
- if (!getSetting('AUTH_ENABLED')) {
+ const clientId = process.env.PLOTLY_CONNECTOR_OAUTH2_CLIENT_ID ||
+ 'isFcew9naom2f1khSiMeAtzuOvHXHuLwhPsM7oPt';
+ res.setCookie('db-connector-oauth2-client-id', clientId, getUnsecuredCookieOptions());
+
+ const authEnabled = getSetting('AUTH_ENABLED');
+ res.setCookie('db-connector-auth-enabled', authEnabled, getUnsecuredCookieOptions());
+
+ if (!authEnabled) {
return next();
}
diff --git a/backend/preload.js b/backend/preload.js
new file mode 100644
index 000000000..2442de269
--- /dev/null
+++ b/backend/preload.js
@@ -0,0 +1,54 @@
+// this file is preloaded by electron in the renderer process
+// (it has access to electron's API, even when nodeIntegration has been disabled)
+
+const {ipcRenderer, remote} = require('electron');
+
+const propertyOptions = {
+ configurable: false,
+ enumerable: false,
+ writable: false
+};
+
+process.once('loaded', () => {
+ const $falcon = {};
+
+ Object.defineProperty(window, '$falcon', {
+ value: $falcon,
+ ...propertyOptions
+ });
+
+ Object.defineProperty($falcon, 'showOpenDialog', {
+ value: remote.dialog.showOpenDialog,
+ ...propertyOptions
+ });
+
+ let onUsername;
+ let receivedUsernameBeforeCallback;
+ let username;
+ function sendUsername(event, user) {
+ try {
+ if (onUsername) {
+ receivedUsernameBeforeCallback = false;
+ onUsername(event, user);
+ } else {
+ receivedUsernameBeforeCallback = true;
+ username = user;
+ }
+ } catch (error) {
+ console.error(error); // eslint-disable-line
+ }
+ }
+
+ ipcRenderer.on('username', sendUsername);
+
+ Object.defineProperty($falcon, 'setUsernameListener', {
+ value: (value) => {
+ onUsername = value;
+
+ if (receivedUsernameBeforeCallback) {
+ sendUsername(null, username);
+ }
+ },
+ ...propertyOptions
+ });
+});
diff --git a/backend/routes.js b/backend/routes.js
index 296f3cff6..935a6d4c3 100644
--- a/backend/routes.js
+++ b/backend/routes.js
@@ -1,13 +1,24 @@
+const fetch = require('node-fetch');
+import {contains, keys, isEmpty, merge, pluck} from 'ramda';
const restify = require('restify');
const CookieParser = require('restify-cookies');
-const fetch = require('node-fetch');
import * as fs from 'fs';
import path from 'path';
-import * as Datastores from './persistent/datastores/Datastores.js';
import {PlotlyOAuth} from './plugins/authorization.js';
-import {getQueries, getQuery, deleteQuery} from './persistent/Queries';
+import {generateAndSaveAccessToken} from './utils/authUtils.js';
+import {
+ getAccessTokenCookieOptions,
+ getCookieOptions,
+ getUnsecuredCookieOptions
+} from './constants.js';
+import {getCerts, timeoutFetchAndSaveCerts, setRenewalJob} from './certificates.js';
+import * as Datastores from './persistent/datastores/Datastores.js';
+import init from './init.js';
+import Logger from './logger.js';
+import {checkWritePermissions, newDatacache} from './persistent/plotly-api.js';
+import {getQueries, getQuery, deleteQuery} from './persistent/Queries.js';
import {
deleteConnectionById,
editConnectionById,
@@ -21,13 +32,7 @@ import {
} from './persistent/Connections.js';
import QueryScheduler from './persistent/QueryScheduler.js';
import {getSetting, saveSetting} from './settings.js';
-import {generateAndSaveAccessToken} from './utils/authUtils';
-import {getAccessTokenCookieOptions, getCookieOptions} from './constants';
-import {checkWritePermissions, newDatacache} from './persistent/plotly-api.js';
-import {contains, keys, isEmpty, merge, pluck} from 'ramda';
-import {getCerts, timeoutFetchAndSaveCerts, setRenewalJob} from './certificates';
-import Logger from './logger';
-import init from './init.js';
+
export default class Servers {
/*
@@ -309,7 +314,7 @@ export default class Servers {
res.setCookie('db-connector-auth-token',
db_connector_access_token,
getAccessTokenCookieOptions());
- res.setCookie('db-connector-user', username, getCookieOptions());
+ res.setCookie('db-connector-user', username, getUnsecuredCookieOptions());
const existingUsers = getSetting('USERS');
const existingUsernames = pluck('username', existingUsers);
@@ -613,13 +618,15 @@ export default class Servers {
}
else {
const rand = Math.round(Math.random() * 1000).toString();
- const downloadPath = path.join(getSetting('STORAGE_PATH'), `data_export_${rand}.csv`);
+ const downloadPath = path.resolve(
+ path.join(getSetting('STORAGE_PATH'), `data_export_${rand}.csv`)
+ );
fs.writeFile(downloadPath, payload, (err) => {
if (err) {
res.json({type: 'error', message: err});
return next();
}
- res.json({type: 'csv', url: 'file:///'.concat(downloadPath)});
+ res.json({type: 'csv', url: 'file://'.concat(downloadPath)});
return next();
});
}
diff --git a/package.json b/package.json
index b3d18c06b..909e0972e 100644
--- a/package.json
+++ b/package.json
@@ -236,6 +236,7 @@
"dependencies": {
"alasql": "^0.4.5",
"csv-parse": "^2.0.0",
+ "dtrace-provider": "^0.8.6",
"font-awesome": "^4.6.1",
"ibm_db": "^2.3.0",
"mysql": "^2.15.0",
diff --git a/webpack.config.base.js b/webpack.config.base.js
index f42560b58..5e19e5054 100644
--- a/webpack.config.base.js
+++ b/webpack.config.base.js
@@ -32,6 +32,7 @@ export default {
{
'csv-parse': 'commonjs csv-parse',
'data-urls': 'commonjs data-urls',
+ 'dtrace-provider': 'commonjs dtrace-provider',
'font-awesome': 'font-awesome',
'ibm_db': 'commonjs ibm_db',
'mysql': 'mysql',
diff --git a/webpack.config.web.js b/webpack.config.web.js
index 6b2ddafb9..5f63e8523 100644
--- a/webpack.config.web.js
+++ b/webpack.config.web.js
@@ -2,13 +2,6 @@ import webpack from 'webpack';
import baseConfig from './webpack.config.base';
import path from 'path';
-const AUTH_ENABLED = process.env.PLOTLY_CONNECTOR_AUTH_ENABLED ?
- JSON.parse(process.env.PLOTLY_CONNECTOR_AUTH_ENABLED) : true;
-
-const OAUTH2_CLIENT_ID = process.env.PLOTLY_CONNECTOR_OAUTH2_CLIENT_ID ?
- JSON.stringify(process.env.PLOTLY_CONNECTOR_OAUTH2_CLIENT_ID) :
- JSON.stringify('isFcew9naom2f1khSiMeAtzuOvHXHuLwhPsM7oPt');
-
const config = {
...baseConfig,
@@ -31,16 +24,6 @@ const config = {
new webpack.DefinePlugin({
__DEV__: false,
'process.env.NODE_ENV': JSON.stringify('production')
- }),
-
- // This is used to pass environment variables to frontend
- // detailed discussions on this ticket:
- // https://github.com/plotly/streambed/issues/10436
- new webpack.DefinePlugin({
- 'PLOTLY_ENV': {
- 'AUTH_ENABLED': AUTH_ENABLED,
- 'OAUTH2_CLIENT_ID': OAUTH2_CLIENT_ID
- }
})
],
diff --git a/yarn.lock b/yarn.lock
index 66b223449..9ab787c7e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3096,6 +3096,12 @@ dtrace-provider@^0.8.2, dtrace-provider@~0.8:
dependencies:
nan "^2.3.3"
+dtrace-provider@^0.8.6:
+ version "0.8.7"
+ resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.7.tgz#dc939b4d3e0620cfe0c1cd803d0d2d7ed04ffd04"
+ dependencies:
+ nan "^2.10.0"
+
dtype@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/dtype/-/dtype-1.0.0.tgz#ae34ffa282673715203582d61bbdd0aad3cba3e7"
@@ -6751,6 +6757,10 @@ mysql@^2.15.0:
safe-buffer "5.1.1"
sqlstring "2.3.0"
+nan@^2.10.0:
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
+
nan@^2.3.0, nan@^2.3.3, nan@~2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"