Merge pull request #2501 from getredash/fix-dashboard-filters

Improve dashboard refresh UX: show previous data while refreshing.
This commit is contained in:
Arik Fraimovich
2018-05-02 14:02:59 +03:00
committed by GitHub
9 changed files with 284 additions and 249 deletions

View File

@@ -94,11 +94,10 @@ const AddWidgetDialog = {
widget.options.position.col = position.col;
widget.options.position.row = position.row;
widget.$save()
.then((response) => {
// update dashboard layout
this.dashboard.version = response.version;
this.dashboard.widgets.push(new Widget(response.widget));
widget
.save()
.then(() => {
this.dashboard.widgets.push(widget);
this.close();
})
.catch(() => {

View File

@@ -10,7 +10,7 @@
</div>
<div class="dropdown pull-right widget-menu-regular" ng-if="!$ctrl.public" uib-dropdown>
<div class="actions">
<a data-toggle="dropdown" uib-dropdown-toggle><i class="zmdi zmdi-more"></i></a>
<a data-toggle="dropdown" uib-dropdown-toggle><i class="zmdi zmdi-more-vert"></i></a>
</div>
<ul class="dropdown-menu pull-right" uib-dropdown-menu style="z-index:1000000">
@@ -51,8 +51,10 @@
</div>
<div class="body-row clearfix tile__bottom-control">
<a class="small hidden-print" ng-click="$ctrl.reload(true)" ng-if="!$ctrl.public">
<i class="zmdi zmdi-time-restore"></i> <span am-time-ago="$ctrl.widget.getQueryResult().getUpdatedAt()"></span>
<a class="small hidden-print" ng-click="$ctrl.refresh()" ng-if="!$ctrl.public">
<i ng-class='{"zmdi-hc-spin": $ctrl.widget.loading}' class="zmdi zmdi-refresh"></i>
<span am-time-ago="$ctrl.widget.getQueryResult().getUpdatedAt()" ng-if="!$ctrl.widget.loading"></span>
<rd-timer timestamp="$ctrl.widget.refreshStartedAt" ng-if="$ctrl.widget.loading"></rd-timer>
</a>
<span class="small hidden-print" ng-if="$ctrl.public">
<i class="zmdi zmdi-time-restore"></i> <span am-time-ago="$ctrl.widget.getQueryResult().getUpdatedAt()"></span>
@@ -61,7 +63,7 @@
<i class="zmdi zmdi-time-restore"></i> {{$ctrl.widget.getQueryResult().getUpdatedAt() | dateTime}}
</span>
<button class="btn btn-sm btn-default pull-right hidden-print btn-transparent btn__refresh" ng-click="$ctrl.reload(true)" ng-if="!$ctrl.public"><i class="zmdi zmdi-refresh"></i></button>
<button class="btn btn-sm btn-default pull-right hidden-print btn-transparent btn__refresh" ng-click="$ctrl.refresh()" ng-if="!$ctrl.public"><i class="zmdi zmdi-refresh"></i></button>
</div>
</div>

View File

@@ -19,7 +19,7 @@ const EditTextBoxComponent = {
if (this.widget.new_text !== this.widget.existing_text) {
this.widget.text = this.widget.new_text;
this.widget
.$save()
.save()
.then(() => {
this.close();
})
@@ -67,9 +67,7 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
Events.record('delete', 'widget', this.widget.id);
this.widget.$delete((response) => {
this.dashboard.widgets = this.dashboard.widgets.filter(w => w.id !== undefined && w.id !== this.widget.id);
this.dashboard.version = response.version;
this.widget.delete().then(() => {
if (this.deleted) {
this.deleted({});
}
@@ -78,18 +76,21 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
Events.record('view', 'widget', this.widget.id);
this.reload = (force) => {
this.load = (refresh = false) => {
const maxAge = $location.search().maxAge;
this.widget.load(force, maxAge);
this.widget.load(refresh, maxAge);
};
this.refresh = () => {
this.load(true);
};
if (this.widget.visualization) {
Events.record('view', 'query', this.widget.visualization.query.id, { dashboard: true });
Events.record('view', 'visualization', this.widget.visualization.id, { dashboard: true });
this.reload(false);
this.type = 'visualization';
this.load();
} else if (this.widget.restricted) {
this.type = 'restricted';
} else {

View File

@@ -31,7 +31,7 @@
<div class="btn-group" uib-dropdown ng-if="!$ctrl.layoutEditing">
<button id="split-button" type="button"
ng-class="{'btn-default btn-sm': $ctrl.refreshRate === null,'btn-primary btn-sm':$ctrl.refreshRate !== null}"
class="btn btn-sm" ng-click="$ctrl.loadDashboard(true)">
class="btn btn-sm" ng-click="$ctrl.refreshDashboard()">
<i class="zmdi zmdi-refresh"></i> {{$ctrl.refreshRate === null ? 'Refresh' : $ctrl.refreshRate.name}}
</button>
<button type="button" class="btn" uib-dropdown-toggle
@@ -92,7 +92,7 @@
ng-repeat="widget in $ctrl.dashboard.widgets track by widget.id"
gridstack-item="widget.options.position" gridstack-item-id="{{ widget.id }}">
<div class="grid-stack-item-content">
<dashboard-widget widget="widget" dashboard="$ctrl.dashboard" on-delete="$ctrl.removeWidget()"></dashboard-widget>
<dashboard-widget widget="widget" dashboard="$ctrl.dashboard" on-delete="$ctrl.removeWidget(widget.id)"></dashboard-widget>
</div>
</div>
</div>

View File

@@ -46,7 +46,7 @@ function DashboardCtrl(
this.saveInProgress = true;
const showMessages = true;
return $q
.all(_.map(widgets, widget => widget.$save()))
.all(_.map(widgets, widget => widget.save()))
.then(() => {
if (showMessages) {
toastr.success('Changes saved.');
@@ -83,7 +83,7 @@ function DashboardCtrl(
this.refreshRate = rate;
if (rate !== null) {
if (load) {
this.loadDashboard(true);
this.refreshDashboard();
}
this.autoRefresh();
}
@@ -118,7 +118,7 @@ function DashboardCtrl(
};
const collectFilters = (dashboard, forceRefresh) => {
const queryResultPromises = _.compact(this.dashboard.widgets.map(widget => widget.loadPromise(forceRefresh)));
const queryResultPromises = _.compact(this.dashboard.widgets.map(widget => widget.load(forceRefresh)));
$q.all(queryResultPromises).then((queryResults) => {
const filters = {};
@@ -206,9 +206,13 @@ function DashboardCtrl(
this.loadDashboard();
this.refreshDashboard = () => {
renderDashboard(this.dashboard, true);
};
this.autoRefresh = () => {
$timeout(() => {
this.loadDashboard(true);
this.refreshDashboard();
}, this.refreshRate.rate * 1000).then(() => this.autoRefresh());
};
@@ -319,12 +323,13 @@ function DashboardCtrl(
// Save position of newly added widget (but not entire layout)
const widget = _.last(this.dashboard.widgets);
if (_.isObject(widget)) {
return widget.$save();
return widget.save();
}
});
};
this.removeWidget = () => {
this.removeWidget = (widgetId) => {
this.dashboard.widgets = this.dashboard.widgets.filter(w => w.id !== undefined && w.id !== widgetId);
this.extractGlobalParameters();
if (!this.layoutEditing) {
// We need to wait a bit while `angular` updates widgets, and only then save new layout

View File

@@ -1,142 +1,166 @@
import moment from 'moment';
import { truncate } from 'underscore.string';
import { pick, flatten, extend, isObject } from 'underscore';
import { each, pick, extend, isObject } from 'underscore';
function Widget($resource, $http, Query, Visualization, dashboardGridOptions) {
function prepareForSave(data) {
return pick(data, 'options', 'text', 'id', 'width', 'dashboard_id', 'visualization_id');
function calculatePositionOptions(Visualization, dashboardGridOptions, widget) {
widget.width = 1; // Backward compatibility, user on back-end
const visualizationOptions = {
autoHeight: false,
sizeX: Math.round(dashboardGridOptions.columns / 2),
sizeY: dashboardGridOptions.defaultSizeY,
minSizeX: dashboardGridOptions.minSizeX,
maxSizeX: dashboardGridOptions.maxSizeX,
minSizeY: dashboardGridOptions.minSizeY,
maxSizeY: dashboardGridOptions.maxSizeY,
};
const visualization = widget.visualization ? Visualization.visualizations[widget.visualization.type] : null;
if (isObject(visualization)) {
const options = extend({}, visualization.defaultOptions);
if (Object.prototype.hasOwnProperty.call(options, 'autoHeight')) {
visualizationOptions.autoHeight = options.autoHeight;
}
// Width constraints
const minColumns = parseInt(options.minColumns, 10);
if (isFinite(minColumns) && minColumns >= 0) {
visualizationOptions.minSizeX = minColumns;
}
const maxColumns = parseInt(options.maxColumns, 10);
if (isFinite(maxColumns) && maxColumns >= 0) {
visualizationOptions.maxSizeX = Math.min(maxColumns, dashboardGridOptions.columns);
}
// Height constraints
// `minRows` is preferred, but it should be kept for backward compatibility
const height = parseInt(options.height, 10);
if (isFinite(height)) {
visualizationOptions.minSizeY = Math.ceil(height / dashboardGridOptions.rowHeight);
}
const minRows = parseInt(options.minRows, 10);
if (isFinite(minRows)) {
visualizationOptions.minSizeY = minRows;
}
const maxRows = parseInt(options.maxRows, 10);
if (isFinite(maxRows) && maxRows >= 0) {
visualizationOptions.maxSizeY = maxRows;
}
// Default dimensions
const defaultWidth = parseInt(options.defaultColumns, 10);
if (isFinite(defaultWidth) && defaultWidth > 0) {
visualizationOptions.sizeX = defaultWidth;
}
const defaultHeight = parseInt(options.defaultRows, 10);
if (isFinite(defaultHeight) && defaultHeight > 0) {
visualizationOptions.sizeY = defaultHeight;
}
}
const WidgetResource = $resource(
'api/widgets/:id',
{ id: '@id' },
{
get: { method: 'GET' },
save: {
method: 'POST',
transformRequest: flatten([prepareForSave, $http.defaults.transformRequest]),
},
query: { method: 'GET', isArray: true },
remove: { method: 'DELETE' },
delete: { method: 'DELETE' },
},
);
return visualizationOptions;
}
WidgetResource.prototype.getQuery = function getQuery() {
if (!this.query && this.visualization) {
this.query = new Query(this.visualization.query);
function WidgetFactory($http, Query, Visualization, dashboardGridOptions) {
class Widget {
constructor(data) {
// Copy properties
each(data, (v, k) => {
this[k] = v;
});
const visualizationOptions = calculatePositionOptions(Visualization, dashboardGridOptions, this);
this.options = this.options || {};
this.options.position = extend(
{},
visualizationOptions,
pick(this.options.position, ['col', 'row', 'sizeX', 'sizeY', 'autoHeight']),
);
if (this.options.position.sizeY < 0) {
this.options.position.autoHeight = true;
}
// Save original position (create a shallow copy)
this.$originalPosition = extend({}, this.options.position);
}
return this.query;
};
getQuery() {
if (!this.query && this.visualization) {
this.query = new Query(this.visualization.query);
}
WidgetResource.prototype.getQueryResult = function getQueryResult(force, maxAge) {
return this.load(force, maxAge);
};
WidgetResource.prototype.load = function load(force, maxAge) {
if (!this.visualization) {
return undefined;
return this.query;
}
if (force || this.queryResult === undefined) {
if (maxAge === undefined || force) {
maxAge = force ? 0 : undefined;
}
this.queryResult = this.getQuery().getQueryResult(maxAge);
getQueryResult() {
return this.data;
}
return this.queryResult;
};
WidgetResource.prototype.loadPromise = function loadPromise(force, maxAge) {
return this.load(force, maxAge).toPromise();
};
WidgetResource.prototype.getName = function getName() {
if (this.visualization) {
return `${this.visualization.query.name} (${this.visualization.name})`;
}
return truncate(this.text, 20);
};
function WidgetConstructor(widget) {
widget.width = 1; // Backward compatibility, user on back-end
const visualizationOptions = {
autoHeight: false,
sizeX: Math.round(dashboardGridOptions.columns / 2),
sizeY: dashboardGridOptions.defaultSizeY,
minSizeX: dashboardGridOptions.minSizeX,
maxSizeX: dashboardGridOptions.maxSizeX,
minSizeY: dashboardGridOptions.minSizeY,
maxSizeY: dashboardGridOptions.maxSizeY,
};
const visualization = widget.visualization ? Visualization.visualizations[widget.visualization.type] : null;
if (isObject(visualization)) {
const options = extend({}, visualization.defaultOptions);
if (Object.prototype.hasOwnProperty.call(options, 'autoHeight')) {
visualizationOptions.autoHeight = options.autoHeight;
}
// Width constraints
const minColumns = parseInt(options.minColumns, 10);
if (isFinite(minColumns) && minColumns >= 0) {
visualizationOptions.minSizeX = minColumns;
}
const maxColumns = parseInt(options.maxColumns, 10);
if (isFinite(maxColumns) && maxColumns >= 0) {
visualizationOptions.maxSizeX = Math.min(maxColumns, dashboardGridOptions.columns);
}
// Height constraints
// `minRows` is preferred, but it should be kept for backward compatibility
const height = parseInt(options.height, 10);
if (isFinite(height)) {
visualizationOptions.minSizeY = Math.ceil(height / dashboardGridOptions.rowHeight);
}
const minRows = parseInt(options.minRows, 10);
if (isFinite(minRows)) {
visualizationOptions.minSizeY = minRows;
}
const maxRows = parseInt(options.maxRows, 10);
if (isFinite(maxRows) && maxRows >= 0) {
visualizationOptions.maxSizeY = maxRows;
}
// Default dimensions
const defaultWidth = parseInt(options.defaultColumns, 10);
if (isFinite(defaultWidth) && defaultWidth > 0) {
visualizationOptions.sizeX = defaultWidth;
}
const defaultHeight = parseInt(options.defaultRows, 10);
if (isFinite(defaultHeight) && defaultHeight > 0) {
visualizationOptions.sizeY = defaultHeight;
getName() {
if (this.visualization) {
return `${this.visualization.query.name} (${this.visualization.name})`;
}
return truncate(this.text, 20);
}
widget.options = widget.options || {};
widget.options.position = extend(
{},
visualizationOptions,
pick(widget.options.position, ['col', 'row', 'sizeX', 'sizeY', 'autoHeight']),
);
load(force, maxAge) {
this.loading = true;
this.refreshStartedAt = moment();
if (widget.options.position.sizeY < 0) {
widget.options.position.autoHeight = true;
if (!this.visualization) {
return undefined;
}
if (force || this.queryResult === undefined) {
if (maxAge === undefined || force) {
maxAge = force ? 0 : undefined;
}
this.queryResult = this.getQuery().getQueryResult(maxAge);
this.queryResult.toPromise().then(
(queryResult) => {
this.data = queryResult;
this.loading = false;
},
() => {
this.loading = false;
this.data = null;
},
);
}
return this.queryResult.toPromise();
}
const result = new WidgetResource(widget);
save() {
const data = pick(this, 'options', 'text', 'id', 'width', 'dashboard_id', 'visualization_id');
// Save original position (create a shallow copy)
result.$originalPosition = extend({}, result.options.position);
let url = 'api/widgets';
if (this.id) {
url = `${url}/${this.id}`;
}
return result;
return $http.post(url, data).then((response) => {
each(response.data, (v, k) => {
this[k] = v;
});
return this;
});
}
delete() {
const url = `api/widgets/${this.id}`;
return $http.delete(url);
}
}
return WidgetConstructor;
return Widget;
}
export default function init(ngModule) {
ngModule.factory('Widget', Widget);
ngModule.factory('Widget', WidgetFactory);
}

View File

@@ -44,7 +44,7 @@ class WidgetListResource(BaseResource):
models.db.session.commit()
models.db.session.commit()
return {'widget': widget.to_dict()}
return widget.to_dict()
class WidgetResource(BaseResource):

View File

@@ -54,7 +54,7 @@ class WidgetAPITest(BaseTestCase):
rv = self.make_request('post', '/api/widgets', data=data)
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.json['widget']['text'], 'Sample text.')
self.assertEquals(rv.json['text'], 'Sample text.')
def test_delete_widget(self):
widget = self.factory.create_widget()

View File

@@ -1,94 +1,87 @@
/* eslint-disable */
const fs = require('fs');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const fs = require("fs");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const WebpackBuildNotifierPlugin = require('webpack-build-notifier');
const ManifestPlugin = require('webpack-manifest-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const LessPluginAutoPrefix = require('less-plugin-autoprefix');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const path = require('path');
const WebpackBuildNotifierPlugin = require("webpack-build-notifier");
const ManifestPlugin = require("webpack-manifest-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const LessPluginAutoPrefix = require("less-plugin-autoprefix");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const path = require("path");
const redashBackend = process.env.REDASH_BACKEND || 'http://localhost:5000';
const redashBackend = process.env.REDASH_BACKEND || "http://localhost:5000";
const basePath = fs.realpathSync(path.join(__dirname, 'client'));
const appPath = fs.realpathSync(path.join(__dirname, 'client', 'app'));
const basePath = fs.realpathSync(path.join(__dirname, "client"));
const appPath = fs.realpathSync(path.join(__dirname, "client", "app"));
const config = {
entry: {
app: [
'./client/app/index.js',
'./client/app/assets/less/main.less',
],
server: [
'./client/app/assets/less/server.less',
],
app: ["./client/app/index.js", "./client/app/assets/less/main.less"],
server: ["./client/app/assets/less/server.less"]
},
output: {
path: path.join(basePath, './dist'),
filename: '[name].js',
publicPath: '/static/'
path: path.join(basePath, "./dist"),
filename: "[name].js",
publicPath: "/static/"
},
resolve: {
alias: {
'@': appPath,
"@": appPath,
// Currently `lodash` is used only by `gridstack.js`, but it can work
// with `underscore` as well, so set an alias to avoid bundling both `lodash` and
// `underscore`. When adding new libraries, check if they can work
// with `underscore`, otherwise remove this line
'lodash': 'underscore',
lodash: "underscore"
}
},
plugins: [
new WebpackBuildNotifierPlugin({title: 'Redash'}),
new WebpackBuildNotifierPlugin({ title: "Redash" }),
new webpack.DefinePlugin({
ON_TEST: process.env.NODE_ENV === 'test'
ON_TEST: process.env.NODE_ENV === "test"
}),
// Enforce angular to use jQuery instead of jqLite
new webpack.ProvidePlugin({'window.jQuery': 'jquery'}),
new webpack.ProvidePlugin({ "window.jQuery": "jquery" }),
// bundle only default `moment` locale (`en`)
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
name: "vendor",
minChunks: function(module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, './node_modules')
) === 0
)
module.resource.indexOf(path.join(__dirname, "./node_modules")) === 0
);
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
name: "manifest",
chunks: ["vendor"]
}),
new HtmlWebpackPlugin({
template: './client/app/index.html',
filename: 'index.html',
excludeChunks: ['server'],
template: "./client/app/index.html",
filename: "index.html",
excludeChunks: ["server"]
}),
new HtmlWebpackPlugin({
template: './client/app/multi_org.html',
filename: 'multi_org.html',
excludeChunks: ['server'],
template: "./client/app/multi_org.html",
filename: "multi_org.html",
excludeChunks: ["server"]
}),
new ExtractTextPlugin({
filename: '[name].[chunkhash].css',
filename: "[name].[chunkhash].css"
}),
new ManifestPlugin({
fileName: 'asset-manifest.json'
fileName: "asset-manifest.json"
}),
new CopyWebpackPlugin([
{ from: 'client/app/assets/robots.txt' },
{ from: 'client/app/assets/css/login.css', to: 'styles/login.css' },
{ from: 'node_modules/jquery/dist/jquery.min.js', to: 'js/jquery.min.js' },
{ from: "client/app/assets/robots.txt" },
{ from: "client/app/assets/css/login.css", to: "styles/login.css" },
{ from: "node_modules/jquery/dist/jquery.min.js", to: "js/jquery.min.js" }
])
],
@@ -97,113 +90,122 @@ const config = {
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader', 'eslint-loader']
use: ["babel-loader", "eslint-loader"]
},
{
test: /\.html$/,
exclude: [/node_modules/, /index\.html/],
use: [{
loader: 'raw-loader'
}]
use: [
{
loader: "raw-loader"
}
]
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract([{
loader: 'css-loader',
options: {
minimize: process.env.NODE_ENV === 'production'
use: ExtractTextPlugin.extract([
{
loader: "css-loader",
options: {
minimize: process.env.NODE_ENV === "production"
}
}
}])
])
},
{
test: /\.less$/,
use: ExtractTextPlugin.extract([
{
loader: 'css-loader',
loader: "css-loader",
options: {
minimize: process.env.NODE_ENV === 'production'
minimize: process.env.NODE_ENV === "production"
}
}, {
loader: 'less-loader',
},
{
loader: "less-loader",
options: {
plugins: [
new LessPluginAutoPrefix({browsers: ['last 3 versions']})
]
plugins: [new LessPluginAutoPrefix({ browsers: ["last 3 versions"] })]
}
}
])
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [{
loader: 'file-loader',
options: {
context: path.resolve(appPath, './assets/images/'),
outputPath: 'images/',
name: '[path][name].[ext]',
use: [
{
loader: "file-loader",
options: {
context: path.resolve(appPath, "./assets/images/"),
outputPath: "images/",
name: "[path][name].[ext]"
}
}
}]
]
},
{
test: /\.geo\.json$/,
use: [{
loader: 'file-loader',
options: {
outputPath: 'data/',
name: '[hash:7].[name].[ext]',
use: [
{
loader: "file-loader",
options: {
outputPath: "data/",
name: "[hash:7].[name].[ext]"
}
}
}]
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: [{
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name].[hash:7].[ext]'
use: [
{
loader: "url-loader",
options: {
limit: 10000,
name: "fonts/[name].[hash:7].[ext]"
}
}
}]
]
}
]
},
devtool: 'cheap-eval-module-source-map',
devtool: "cheap-eval-module-source-map",
stats: {
modules: false,
chunkModules: false,
chunkModules: false
},
watchOptions: {
ignored: /\.sw.$/,
ignored: /\.sw.$/
},
devServer: {
inline: true,
index: '/static/index.html',
index: "/static/index.html",
historyApiFallback: {
index: '/static/index.html',
rewrites: [{from: /./, to: '/static/index.html'}],
index: "/static/index.html",
rewrites: [{ from: /./, to: "/static/index.html" }]
},
contentBase: false,
publicPath: '/static/',
publicPath: "/static/",
proxy: [
{
context: ['/login', '/logout', '/invite', '/setup', '/status.json', '/api', '/oauth'],
target: redashBackend + '/',
changeOrigin: true,
secure: false,
context: ["/login", "/logout", "/invite", "/setup", "/status.json", "/api", "/oauth"],
target: redashBackend + "/",
changeOrigin: false,
secure: false
},
{
context: (path) => {
context: path => {
// CSS/JS for server-rendered pages should be served from backend
return /^\/static\/[a-z]+\.[0-9a-fA-F]+\.(css|js)$/.test(path);
},
target: redashBackend + '/',
target: redashBackend + "/",
changeOrigin: true,
secure: false,
secure: false
}
],
stats: {
modules: false,
chunkModules: false,
},
chunkModules: false
}
}
};
@@ -211,15 +213,17 @@ if (process.env.DEV_SERVER_HOST) {
config.devServer.host = process.env.DEV_SERVER_HOST;
}
if (process.env.NODE_ENV === 'production') {
config.output.filename = '[name].[chunkhash].js';
config.plugins.push(new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
compress: {
warnings: true
}
}));
config.devtool = 'source-map';
if (process.env.NODE_ENV === "production") {
config.output.filename = "[name].[chunkhash].js";
config.plugins.push(
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
compress: {
warnings: true
}
})
);
config.devtool = "source-map";
}
if (process.env.BUNDLE_ANALYZER) {