mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d43bfa9d10 | ||
|
|
b4d7a25c74 | ||
|
|
c8a3985f16 | ||
|
|
3274a5d670 | ||
|
|
8037c21a61 | ||
|
|
ee8f6c1c71 | ||
|
|
bce0e8e547 | ||
|
|
cddf398df4 | ||
|
|
bdb8682646 | ||
|
|
54bef2c05f | ||
|
|
d326f2b46c | ||
|
|
06a7a7e65b | ||
|
|
fc0a074a07 | ||
|
|
44a330a4e6 | ||
|
|
ab284efac1 | ||
|
|
9d4fd75ea8 | ||
|
|
4a9b93a131 | ||
|
|
31a48cfe2b | ||
|
|
9df98b1c29 | ||
|
|
d1bc2fb649 | ||
|
|
df774b0304 | ||
|
|
7ac0ba52dd | ||
|
|
b2decc895f | ||
|
|
b12f3fb133 | ||
|
|
fc30f141ec | ||
|
|
277eb35aea | ||
|
|
10bc9402b7 | ||
|
|
16a07e19cc | ||
|
|
479b34faed | ||
|
|
19eed1580e | ||
|
|
d312dffe2c | ||
|
|
90677b2b51 | ||
|
|
df9bd38c08 | ||
|
|
be86d659ed | ||
|
|
66a4315ce2 | ||
|
|
b896dd461d | ||
|
|
615aea7678 | ||
|
|
10a9978b04 | ||
|
|
3f3a86eac3 | ||
|
|
f055e6750a |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,6 +1,32 @@
|
||||
# Change Log
|
||||
|
||||
## UNRELEASED
|
||||
## v4.0.1 - 2018-05-02
|
||||
|
||||
### Added
|
||||
|
||||
- Log user's screen resolution. @arikfr
|
||||
|
||||
### Changed
|
||||
|
||||
- [Redshift] fix the order of columns in the schema browser. @akiray03
|
||||
- Improve dashboard refresh UX: show previous data while refreshing. @arikfr
|
||||
|
||||
### Fixed
|
||||
|
||||
- Disable fork button to view_only users. @tonyjiangh
|
||||
- Hide overflowing data source and alert destination names. @kocsmy
|
||||
- Login pages were broken on mobile. @kocsmy
|
||||
- Cohort visualization wasn't rendering if value wasn't properly detected as date. @kravets-levko
|
||||
- Dashboard filters setting wasn't persisting. @arikfr
|
||||
- Display nulls and empty values as blank in table numeric fields. @chriszs
|
||||
- Date column on alerts page is labeled "Created By". @dbravender
|
||||
- Bootstrap script was breaking due to incompatability with pip 10. @ariarijp
|
||||
|
||||
### Other
|
||||
|
||||
- Updated README. @kocsmy
|
||||
|
||||
## v4.0.0 - 2018-04-16
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -19,19 +19,22 @@ Presto, Google Spreadsheets, Cloudera Impala, Hive and custom scripts.
|
||||
**_Redash_** consists of two parts:
|
||||
|
||||
1. **Query Editor**: think of [JS Fiddle](http://jsfiddle.net) for SQL queries. It's your way to share data in the organization in an open way, by sharing both the dataset and the query that generated it. This way everyone can peer review not only the resulting dataset but also the process that generated it. Also it's possible to fork it and generate new datasets and reach new insights.
|
||||
2. **Dashboards/Visualizations**: once you have a dataset, you can create different visualizations out of it, and then combine several visualizations into a single dashboard. Currently it supports charts, pivot table and cohorts.
|
||||
2. **Visualizations and Dashboards**: once you have a dataset, you can create different visualizations out of it, and then combine several visualizations into a single dashboard. Currently Redash supports charts, pivot table, cohorts and [more](https://redash.io/help/user-guide/visualizations/visualization-types).
|
||||
|
||||
## Demo
|
||||
|
||||
<img src="https://cloud.githubusercontent.com/assets/71468/17391289/8e83878e-5a1d-11e6-8938-af9054a33b19.gif" width="60%"/>
|
||||
<img src="https://raw.githubusercontent.com/getredash/website/8e820cd02c73a8ddf4f946a9d293c54fd3fb08b9/website/_assets/images/redash-anim.gif" width="80%"/>
|
||||
|
||||
You can try out the demo instance: http://demo.redash.io/ (login with any Google account).
|
||||
Try out the [demo instance](http://demo.redash.io/) (login with any Google account).
|
||||
|
||||
## Getting Started
|
||||
|
||||
* [Setting up Redash instance](https://redash.io/help-onpremise/setup/setting-up-redash-instance.html) (includes links to ready made AWS/GCE images).
|
||||
* [Documentation](https://redash.io/help/).
|
||||
|
||||
## Supported Data Sources
|
||||
|
||||
Redash supports more than 25 [data sources](https://redash.io/help/data-sources/supported-data-sources).
|
||||
|
||||
## Getting Help
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// The real magic ;)
|
||||
> a {
|
||||
pointer-events: none;
|
||||
color: @dropdown-link-disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +30,6 @@
|
||||
|
||||
&:not([class*="bg-"]) {
|
||||
& > li > a {
|
||||
color: #4C4C4C;
|
||||
|
||||
&:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// General
|
||||
body {
|
||||
padding-top: 0;
|
||||
background: #cad1dc2b;
|
||||
background: #F6F8F9;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important;
|
||||
|
||||
&.headless {
|
||||
@@ -178,6 +178,8 @@ body {
|
||||
font-size: 13px;
|
||||
color: #323232;
|
||||
margin: 0 !important;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,12 @@ hr {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.fixed-width-page {
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
|
||||
.login-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -47,7 +47,7 @@ function createBooleanFormatter(values) {
|
||||
function createNumberFormatter(format) {
|
||||
if (_.isString(format) && (format !== '')) {
|
||||
const n = numeral(0); // cache `numeral` instance
|
||||
return value => n.set(value).format(format);
|
||||
return value => (value === null || value === '' ? '' : n.set(value).format(format));
|
||||
}
|
||||
return value => value;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<th class="sortable-column" ng-click="$ctrl.alerts.orderBy('name')">Name <sort-icon column="'name'" sort-column="$ctrl.alerts.orderByField" reverse="$ctrl.alerts.orderByReverse"></sort-icon></th>
|
||||
<th class="sortable-column" ng-click="$ctrl.alerts.orderBy('created_by')">Created By <sort-icon column="'created_by'" sort-column="$ctrl.alerts.orderByField" reverse="$ctrl.alerts.orderByReverse"></sort-icon></th>
|
||||
<th class="sortable-column" ng-click="$ctrl.alerts.orderBy('state')">State <sort-icon column="'state'" sort-column="$ctrl.alerts.orderByField" reverse="$ctrl.alerts.orderByReverse"></sort-icon></th>
|
||||
<th class="sortable-column" ng-click="$ctrl.alerts.orderBy('created_at')">Created By <sort-icon column="'created_at'" sort-column="$ctrl.alerts.orderByField" reverse="$ctrl.alerts.orderByReverse"></sort-icon></th>
|
||||
<th class="sortable-column" ng-click="$ctrl.alerts.orderBy('created_at')">Created At <sort-icon column="'created_at'" sort-column="$ctrl.alerts.orderByField" reverse="$ctrl.alerts.orderByReverse"></sort-icon></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -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());
|
||||
};
|
||||
|
||||
@@ -282,8 +286,28 @@ function DashboardCtrl(
|
||||
};
|
||||
|
||||
this.updateDashboardFiltersState = () => {
|
||||
// / do something for humanity.
|
||||
collectFilters(this.dashboard, false);
|
||||
Dashboard.save(
|
||||
{
|
||||
slug: this.dashboard.id,
|
||||
version: this.dashboard.version,
|
||||
dashboard_filters_enabled: this.dashboard.dashboard_filters_enabled,
|
||||
},
|
||||
(dashboard) => {
|
||||
this.dashboard = dashboard;
|
||||
},
|
||||
(error) => {
|
||||
if (error.status === 403) {
|
||||
toastr.error('Name update failed: Permission denied.');
|
||||
} else if (error.status === 409) {
|
||||
toastr.error(
|
||||
'It seems like the dashboard has been modified by another user. ' +
|
||||
'Please copy/backup your changes and reload this page.',
|
||||
{ autoDismiss: false },
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
this.addWidget = () => {
|
||||
@@ -299,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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<a href="data_sources/new" class="btn btn-default"><i class="fa fa-plus"></i> New Data Source</a>
|
||||
</p>
|
||||
<div class="database-source">
|
||||
<a class="visual-card" ng-href="data_sources/{{dataSource.id}}" ng-repeat="dataSource in dataSources">
|
||||
<a class="visual-card" ng-href="data_sources/{{dataSource.id}}" ng-repeat="dataSource in dataSources" title="{{dataSource.name}}">
|
||||
<img ng-src="/static/images/db-logos/{{dataSource.type}}.png" alt="{{dataSource.name}}">
|
||||
<h3>{{dataSource.name}}</h3>
|
||||
</a>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<span class="zmdi zmdi-more"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu pull-right" uib-dropdown-menu>
|
||||
<li><a ng-click="duplicateQuery()" ng-disabled="query.id === undefined || !canForkQuery()"> Fork</a></li>
|
||||
<li ng-class="{'disabled': query.id === undefined || !canForkQuery() }"><a ng-click="duplicateQuery()"> Fork</a></li>
|
||||
<li class="divider"></li>
|
||||
<li ng-if="!query.is_archived && query.id != undefined && (isQueryOwner || currentUser.hasPermission('admin'))"><a ng-click="archiveQuery()">Archive</a></li>
|
||||
<li ng-if="!query.is_archived && query.id != undefined && (isQueryOwner || currentUser.hasPermission('admin')) && showPermissionsControl"><a ng-click="showManagePermissionsModal()">Manage Permissions</a></li>
|
||||
|
||||
@@ -145,6 +145,8 @@ function QueryViewCtrl(
|
||||
|
||||
$scope.canExecuteQuery = () => currentUser.hasPermission('execute_query') && !$scope.dataSource.view_only;
|
||||
|
||||
$scope.canForkQuery = () => currentUser.hasPermission('edit_query') && !$scope.dataSource.view_only;
|
||||
|
||||
$scope.canScheduleQuery = currentUser.hasPermission('schedule_query');
|
||||
|
||||
if ($route.current.locals.dataSources) {
|
||||
|
||||
@@ -16,6 +16,7 @@ function Events($http) {
|
||||
object_type: objectType,
|
||||
object_id: objectId,
|
||||
timestamp: Date.now() / 1000.0,
|
||||
screen_resolution: `${window.screen.width}x${window.screen.height}`,
|
||||
};
|
||||
Object.assign(event, additionalProperties);
|
||||
this.events.push(event);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ function groupData(sortedData) {
|
||||
_.each(sortedData, (item) => {
|
||||
const groupKey = item.date + 0;
|
||||
result[groupKey] = result[groupKey] || {
|
||||
date: item.date,
|
||||
date: moment(item.date),
|
||||
total: parseInt(item.total, 10),
|
||||
values: {},
|
||||
};
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "redash-client",
|
||||
"version": "4.0.0-rc.1",
|
||||
"version": "4.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -7991,6 +7991,11 @@
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.2.0.tgz",
|
||||
"integrity": "sha512-Bold8phAE6WcRsuwhofrQ7cOK1REFHaYIkKuj7+TBYK3ONKRpGGIb5oXR5akYotFnrWN0TWKh6Svlhflm3dogg=="
|
||||
},
|
||||
"leaflet-fullscreen": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/leaflet-fullscreen/-/leaflet-fullscreen-1.0.2.tgz",
|
||||
"integrity": "sha1-CcYcS6xF9jsu4Sav2H5c2XZQ/Bs="
|
||||
},
|
||||
"leaflet.markercluster": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.1.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "redash-client",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.1",
|
||||
"description": "The frontend part of Redash.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -19,7 +19,7 @@ from redash.query_runner import import_query_runners
|
||||
from redash.destinations import import_destinations
|
||||
|
||||
|
||||
__version__ = '4.0.0'
|
||||
__version__ = '4.0.1'
|
||||
|
||||
|
||||
def setup_logging():
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -236,15 +236,24 @@ class Redshift(PostgreSQL):
|
||||
# Use PG_GET_LATE_BINDING_VIEW_COLS to include schema for late binding views data for Redshift
|
||||
# http://docs.aws.amazon.com/redshift/latest/dg/PG_GET_LATE_BINDING_VIEW_COLS.html
|
||||
query = """
|
||||
SELECT DISTINCT table_name, table_schema, column_name
|
||||
FROM svv_columns
|
||||
WHERE table_schema NOT IN ('pg_internal','pg_catalog','information_schema')
|
||||
UNION ALL
|
||||
SELECT DISTINCT view_name::varchar AS table_name,
|
||||
view_schema::varchar AS table_schema,
|
||||
col_name::varchar AS column_name
|
||||
FROM pg_get_late_binding_view_cols()
|
||||
cols(view_schema name, view_name name, col_name name, col_type varchar, col_num int);
|
||||
WITH tables AS (
|
||||
SELECT DISTINCT table_name,
|
||||
table_schema,
|
||||
column_name,
|
||||
ordinal_position AS pos
|
||||
FROM svv_columns
|
||||
WHERE table_schema NOT IN ('pg_internal','pg_catalog','information_schema')
|
||||
UNION ALL
|
||||
SELECT DISTINCT view_name::varchar AS table_name,
|
||||
view_schema::varchar AS table_schema,
|
||||
col_name::varchar AS column_name,
|
||||
col_num AS pos
|
||||
FROM pg_get_late_binding_view_cols()
|
||||
cols(view_schema name, view_name name, col_name name, col_type varchar, col_num int)
|
||||
)
|
||||
SELECT table_name, table_schema, column_name
|
||||
FROM tables
|
||||
ORDER BY table_name, pos
|
||||
"""
|
||||
|
||||
self._get_definitions(schema, query)
|
||||
|
||||
@@ -13,17 +13,17 @@
|
||||
</head>
|
||||
<body class="d-flex flex-column justify-content-center align-items-center">
|
||||
|
||||
<div class="d-flex flex-column justify-content-center align-items-center">
|
||||
<div class="header">
|
||||
<div class="text-center m-b-15">
|
||||
<a href="/"><img src="/static/images/redash_icon_small.png"></a>
|
||||
<div class="d-flex flex-column justify-content-center align-items-center">
|
||||
<div class="header">
|
||||
<div class="text-center m-b-15">
|
||||
<a href="/"><img src="/static/images/redash_icon_small.png"></a>
|
||||
</div>
|
||||
{% if not hide_page_header %}
|
||||
<h3 class="text-center m-t-0 m-b-25">{{ self.title() }}</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not hide_page_header %}
|
||||
<h3 class="text-center m-t-0 m-b-25">{{ self.title() }}</h3>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="/static/js/jquery.min.js"></script>
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ extract_redash_sources() {
|
||||
}
|
||||
|
||||
install_python_packages() {
|
||||
pip install --upgrade pip
|
||||
pip install --upgrade pip==9.0.3
|
||||
# TODO: venv?
|
||||
pip install setproctitle # setproctitle is used by Celery for "pretty" process titles
|
||||
pip install -r $REDASH_BASE_PATH/current/requirements.txt
|
||||
|
||||
@@ -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