mirror of
https://github.com/getredash/redash.git
synced 2025-12-25 01:03:20 -05:00
Merge pull request #2501 from getredash/fix-dashboard-filters
Improve dashboard refresh UX: show previous data while refreshing.
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user