mirror of
https://github.com/getredash/redash.git
synced 2025-12-20 01:47:39 -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
|
# 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
|
### Added
|
||||||
|
|
||||||
|
|||||||
@@ -19,19 +19,22 @@ Presto, Google Spreadsheets, Cloudera Impala, Hive and custom scripts.
|
|||||||
**_Redash_** consists of two parts:
|
**_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.
|
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
|
## 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
|
## 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).
|
* [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/).
|
* [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
|
## Getting Help
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
// The real magic ;)
|
// The real magic ;)
|
||||||
> a {
|
> a {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
color: @dropdown-link-disabled-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,8 +30,6 @@
|
|||||||
|
|
||||||
&:not([class*="bg-"]) {
|
&:not([class*="bg-"]) {
|
||||||
& > li > a {
|
& > li > a {
|
||||||
color: #4C4C4C;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
// General
|
// General
|
||||||
body {
|
body {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
background: #cad1dc2b;
|
background: #F6F8F9;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important;
|
||||||
|
|
||||||
&.headless {
|
&.headless {
|
||||||
@@ -178,6 +178,8 @@ body {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #323232;
|
color: #323232;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ hr {
|
|||||||
width: 500px;
|
width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.fixed-width-page {
|
||||||
|
width: 80vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.login-button {
|
.login-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -94,11 +94,10 @@ const AddWidgetDialog = {
|
|||||||
widget.options.position.col = position.col;
|
widget.options.position.col = position.col;
|
||||||
widget.options.position.row = position.row;
|
widget.options.position.row = position.row;
|
||||||
|
|
||||||
widget.$save()
|
widget
|
||||||
.then((response) => {
|
.save()
|
||||||
// update dashboard layout
|
.then(() => {
|
||||||
this.dashboard.version = response.version;
|
this.dashboard.widgets.push(widget);
|
||||||
this.dashboard.widgets.push(new Widget(response.widget));
|
|
||||||
this.close();
|
this.close();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="dropdown pull-right widget-menu-regular" ng-if="!$ctrl.public" uib-dropdown>
|
<div class="dropdown pull-right widget-menu-regular" ng-if="!$ctrl.public" uib-dropdown>
|
||||||
<div class="actions">
|
<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>
|
</div>
|
||||||
|
|
||||||
<ul class="dropdown-menu pull-right" uib-dropdown-menu style="z-index:1000000">
|
<ul class="dropdown-menu pull-right" uib-dropdown-menu style="z-index:1000000">
|
||||||
@@ -51,8 +51,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="body-row clearfix tile__bottom-control">
|
<div class="body-row clearfix tile__bottom-control">
|
||||||
<a class="small hidden-print" ng-click="$ctrl.reload(true)" ng-if="!$ctrl.public">
|
<a class="small hidden-print" ng-click="$ctrl.refresh()" ng-if="!$ctrl.public">
|
||||||
<i class="zmdi zmdi-time-restore"></i> <span am-time-ago="$ctrl.widget.getQueryResult().getUpdatedAt()"></span>
|
<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>
|
</a>
|
||||||
<span class="small hidden-print" ng-if="$ctrl.public">
|
<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>
|
<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}}
|
<i class="zmdi zmdi-time-restore"></i> {{$ctrl.widget.getQueryResult().getUpdatedAt() | dateTime}}
|
||||||
</span>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const EditTextBoxComponent = {
|
|||||||
if (this.widget.new_text !== this.widget.existing_text) {
|
if (this.widget.new_text !== this.widget.existing_text) {
|
||||||
this.widget.text = this.widget.new_text;
|
this.widget.text = this.widget.new_text;
|
||||||
this.widget
|
this.widget
|
||||||
.$save()
|
.save()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.close();
|
this.close();
|
||||||
})
|
})
|
||||||
@@ -67,9 +67,7 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
|
|||||||
|
|
||||||
Events.record('delete', 'widget', this.widget.id);
|
Events.record('delete', 'widget', this.widget.id);
|
||||||
|
|
||||||
this.widget.$delete((response) => {
|
this.widget.delete().then(() => {
|
||||||
this.dashboard.widgets = this.dashboard.widgets.filter(w => w.id !== undefined && w.id !== this.widget.id);
|
|
||||||
this.dashboard.version = response.version;
|
|
||||||
if (this.deleted) {
|
if (this.deleted) {
|
||||||
this.deleted({});
|
this.deleted({});
|
||||||
}
|
}
|
||||||
@@ -78,18 +76,21 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
|
|||||||
|
|
||||||
Events.record('view', 'widget', this.widget.id);
|
Events.record('view', 'widget', this.widget.id);
|
||||||
|
|
||||||
this.reload = (force) => {
|
this.load = (refresh = false) => {
|
||||||
const maxAge = $location.search().maxAge;
|
const maxAge = $location.search().maxAge;
|
||||||
this.widget.load(force, maxAge);
|
this.widget.load(refresh, maxAge);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.refresh = () => {
|
||||||
|
this.load(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.widget.visualization) {
|
if (this.widget.visualization) {
|
||||||
Events.record('view', 'query', this.widget.visualization.query.id, { dashboard: true });
|
Events.record('view', 'query', this.widget.visualization.query.id, { dashboard: true });
|
||||||
Events.record('view', 'visualization', this.widget.visualization.id, { dashboard: true });
|
Events.record('view', 'visualization', this.widget.visualization.id, { dashboard: true });
|
||||||
|
|
||||||
this.reload(false);
|
|
||||||
|
|
||||||
this.type = 'visualization';
|
this.type = 'visualization';
|
||||||
|
this.load();
|
||||||
} else if (this.widget.restricted) {
|
} else if (this.widget.restricted) {
|
||||||
this.type = 'restricted';
|
this.type = 'restricted';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function createBooleanFormatter(values) {
|
|||||||
function createNumberFormatter(format) {
|
function createNumberFormatter(format) {
|
||||||
if (_.isString(format) && (format !== '')) {
|
if (_.isString(format) && (format !== '')) {
|
||||||
const n = numeral(0); // cache `numeral` instance
|
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;
|
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('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('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('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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<div class="btn-group" uib-dropdown ng-if="!$ctrl.layoutEditing">
|
<div class="btn-group" uib-dropdown ng-if="!$ctrl.layoutEditing">
|
||||||
<button id="split-button" type="button"
|
<button id="split-button" type="button"
|
||||||
ng-class="{'btn-default btn-sm': $ctrl.refreshRate === null,'btn-primary btn-sm':$ctrl.refreshRate !== null}"
|
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}}
|
<i class="zmdi zmdi-refresh"></i> {{$ctrl.refreshRate === null ? 'Refresh' : $ctrl.refreshRate.name}}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn" uib-dropdown-toggle
|
<button type="button" class="btn" uib-dropdown-toggle
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
ng-repeat="widget in $ctrl.dashboard.widgets track by widget.id"
|
ng-repeat="widget in $ctrl.dashboard.widgets track by widget.id"
|
||||||
gridstack-item="widget.options.position" gridstack-item-id="{{ widget.id }}">
|
gridstack-item="widget.options.position" gridstack-item-id="{{ widget.id }}">
|
||||||
<div class="grid-stack-item-content">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ function DashboardCtrl(
|
|||||||
this.saveInProgress = true;
|
this.saveInProgress = true;
|
||||||
const showMessages = true;
|
const showMessages = true;
|
||||||
return $q
|
return $q
|
||||||
.all(_.map(widgets, widget => widget.$save()))
|
.all(_.map(widgets, widget => widget.save()))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (showMessages) {
|
if (showMessages) {
|
||||||
toastr.success('Changes saved.');
|
toastr.success('Changes saved.');
|
||||||
@@ -83,7 +83,7 @@ function DashboardCtrl(
|
|||||||
this.refreshRate = rate;
|
this.refreshRate = rate;
|
||||||
if (rate !== null) {
|
if (rate !== null) {
|
||||||
if (load) {
|
if (load) {
|
||||||
this.loadDashboard(true);
|
this.refreshDashboard();
|
||||||
}
|
}
|
||||||
this.autoRefresh();
|
this.autoRefresh();
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ function DashboardCtrl(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const collectFilters = (dashboard, forceRefresh) => {
|
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) => {
|
$q.all(queryResultPromises).then((queryResults) => {
|
||||||
const filters = {};
|
const filters = {};
|
||||||
@@ -206,9 +206,13 @@ function DashboardCtrl(
|
|||||||
|
|
||||||
this.loadDashboard();
|
this.loadDashboard();
|
||||||
|
|
||||||
|
this.refreshDashboard = () => {
|
||||||
|
renderDashboard(this.dashboard, true);
|
||||||
|
};
|
||||||
|
|
||||||
this.autoRefresh = () => {
|
this.autoRefresh = () => {
|
||||||
$timeout(() => {
|
$timeout(() => {
|
||||||
this.loadDashboard(true);
|
this.refreshDashboard();
|
||||||
}, this.refreshRate.rate * 1000).then(() => this.autoRefresh());
|
}, this.refreshRate.rate * 1000).then(() => this.autoRefresh());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -282,8 +286,28 @@ function DashboardCtrl(
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.updateDashboardFiltersState = () => {
|
this.updateDashboardFiltersState = () => {
|
||||||
// / do something for humanity.
|
|
||||||
collectFilters(this.dashboard, false);
|
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 = () => {
|
this.addWidget = () => {
|
||||||
@@ -299,12 +323,13 @@ function DashboardCtrl(
|
|||||||
// Save position of newly added widget (but not entire layout)
|
// Save position of newly added widget (but not entire layout)
|
||||||
const widget = _.last(this.dashboard.widgets);
|
const widget = _.last(this.dashboard.widgets);
|
||||||
if (_.isObject(widget)) {
|
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();
|
this.extractGlobalParameters();
|
||||||
if (!this.layoutEditing) {
|
if (!this.layoutEditing) {
|
||||||
// We need to wait a bit while `angular` updates widgets, and only then save new layout
|
// 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>
|
<a href="data_sources/new" class="btn btn-default"><i class="fa fa-plus"></i> New Data Source</a>
|
||||||
</p>
|
</p>
|
||||||
<div class="database-source">
|
<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}}">
|
<img ng-src="/static/images/db-logos/{{dataSource.type}}.png" alt="{{dataSource.name}}">
|
||||||
<h3>{{dataSource.name}}</h3>
|
<h3>{{dataSource.name}}</h3>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<span class="zmdi zmdi-more"></span>
|
<span class="zmdi zmdi-more"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu pull-right" uib-dropdown-menu>
|
<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 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'))"><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>
|
<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.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');
|
$scope.canScheduleQuery = currentUser.hasPermission('schedule_query');
|
||||||
|
|
||||||
if ($route.current.locals.dataSources) {
|
if ($route.current.locals.dataSources) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function Events($http) {
|
|||||||
object_type: objectType,
|
object_type: objectType,
|
||||||
object_id: objectId,
|
object_id: objectId,
|
||||||
timestamp: Date.now() / 1000.0,
|
timestamp: Date.now() / 1000.0,
|
||||||
|
screen_resolution: `${window.screen.width}x${window.screen.height}`,
|
||||||
};
|
};
|
||||||
Object.assign(event, additionalProperties);
|
Object.assign(event, additionalProperties);
|
||||||
this.events.push(event);
|
this.events.push(event);
|
||||||
|
|||||||
@@ -1,65 +1,8 @@
|
|||||||
|
import moment from 'moment';
|
||||||
import { truncate } from 'underscore.string';
|
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 calculatePositionOptions(Visualization, dashboardGridOptions, widget) {
|
||||||
function prepareForSave(data) {
|
|
||||||
return pick(data, 'options', 'text', 'id', 'width', 'dashboard_id', 'visualization_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
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' },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
WidgetResource.prototype.getQuery = function getQuery() {
|
|
||||||
if (!this.query && this.visualization) {
|
|
||||||
this.query = new Query(this.visualization.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (force || this.queryResult === undefined) {
|
|
||||||
if (maxAge === undefined || force) {
|
|
||||||
maxAge = force ? 0 : undefined;
|
|
||||||
}
|
|
||||||
this.queryResult = this.getQuery().getQueryResult(maxAge);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
widget.width = 1; // Backward compatibility, user on back-end
|
||||||
|
|
||||||
const visualizationOptions = {
|
const visualizationOptions = {
|
||||||
@@ -71,6 +14,7 @@ function Widget($resource, $http, Query, Visualization, dashboardGridOptions) {
|
|||||||
minSizeY: dashboardGridOptions.minSizeY,
|
minSizeY: dashboardGridOptions.minSizeY,
|
||||||
maxSizeY: dashboardGridOptions.maxSizeY,
|
maxSizeY: dashboardGridOptions.maxSizeY,
|
||||||
};
|
};
|
||||||
|
|
||||||
const visualization = widget.visualization ? Visualization.visualizations[widget.visualization.type] : null;
|
const visualization = widget.visualization ? Visualization.visualizations[widget.visualization.type] : null;
|
||||||
if (isObject(visualization)) {
|
if (isObject(visualization)) {
|
||||||
const options = extend({}, visualization.defaultOptions);
|
const options = extend({}, visualization.defaultOptions);
|
||||||
@@ -115,28 +59,108 @@ function Widget($resource, $http, Query, Visualization, dashboardGridOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
widget.options = widget.options || {};
|
return visualizationOptions;
|
||||||
widget.options.position = extend(
|
}
|
||||||
|
|
||||||
|
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,
|
visualizationOptions,
|
||||||
pick(widget.options.position, ['col', 'row', 'sizeX', 'sizeY', 'autoHeight']),
|
pick(this.options.position, ['col', 'row', 'sizeX', 'sizeY', 'autoHeight']),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.options.position.sizeY < 0) {
|
if (this.options.position.sizeY < 0) {
|
||||||
widget.options.position.autoHeight = true;
|
this.options.position.autoHeight = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = new WidgetResource(widget);
|
|
||||||
|
|
||||||
// Save original position (create a shallow copy)
|
// Save original position (create a shallow copy)
|
||||||
result.$originalPosition = extend({}, result.options.position);
|
this.$originalPosition = extend({}, this.options.position);
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return WidgetConstructor;
|
getQuery() {
|
||||||
|
if (!this.query && this.visualization) {
|
||||||
|
this.query = new Query(this.visualization.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.query;
|
||||||
|
}
|
||||||
|
|
||||||
|
getQueryResult() {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
if (this.visualization) {
|
||||||
|
return `${this.visualization.query.name} (${this.visualization.name})`;
|
||||||
|
}
|
||||||
|
return truncate(this.text, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
load(force, maxAge) {
|
||||||
|
this.loading = true;
|
||||||
|
this.refreshStartedAt = moment();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
const data = pick(this, 'options', 'text', 'id', 'width', 'dashboard_id', 'visualization_id');
|
||||||
|
|
||||||
|
let url = 'api/widgets';
|
||||||
|
if (this.id) {
|
||||||
|
url = `${url}/${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function init(ngModule) {
|
export default function init(ngModule) {
|
||||||
ngModule.factory('Widget', Widget);
|
ngModule.factory('Widget', WidgetFactory);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function groupData(sortedData) {
|
|||||||
_.each(sortedData, (item) => {
|
_.each(sortedData, (item) => {
|
||||||
const groupKey = item.date + 0;
|
const groupKey = item.date + 0;
|
||||||
result[groupKey] = result[groupKey] || {
|
result[groupKey] = result[groupKey] || {
|
||||||
date: item.date,
|
date: moment(item.date),
|
||||||
total: parseInt(item.total, 10),
|
total: parseInt(item.total, 10),
|
||||||
values: {},
|
values: {},
|
||||||
};
|
};
|
||||||
|
|||||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "redash-client",
|
"name": "redash-client",
|
||||||
"version": "4.0.0-rc.1",
|
"version": "4.0.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -7991,6 +7991,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.2.0.tgz",
|
||||||
"integrity": "sha512-Bold8phAE6WcRsuwhofrQ7cOK1REFHaYIkKuj7+TBYK3ONKRpGGIb5oXR5akYotFnrWN0TWKh6Svlhflm3dogg=="
|
"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": {
|
"leaflet.markercluster": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.1.0.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "redash-client",
|
"name": "redash-client",
|
||||||
"version": "4.0.0",
|
"version": "4.0.1",
|
||||||
"description": "The frontend part of Redash.",
|
"description": "The frontend part of Redash.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from redash.query_runner import import_query_runners
|
|||||||
from redash.destinations import import_destinations
|
from redash.destinations import import_destinations
|
||||||
|
|
||||||
|
|
||||||
__version__ = '4.0.0'
|
__version__ = '4.0.1'
|
||||||
|
|
||||||
|
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class WidgetListResource(BaseResource):
|
|||||||
models.db.session.commit()
|
models.db.session.commit()
|
||||||
|
|
||||||
models.db.session.commit()
|
models.db.session.commit()
|
||||||
return {'widget': widget.to_dict()}
|
return widget.to_dict()
|
||||||
|
|
||||||
|
|
||||||
class WidgetResource(BaseResource):
|
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
|
# 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
|
# http://docs.aws.amazon.com/redshift/latest/dg/PG_GET_LATE_BINDING_VIEW_COLS.html
|
||||||
query = """
|
query = """
|
||||||
SELECT DISTINCT table_name, table_schema, column_name
|
WITH tables AS (
|
||||||
|
SELECT DISTINCT table_name,
|
||||||
|
table_schema,
|
||||||
|
column_name,
|
||||||
|
ordinal_position AS pos
|
||||||
FROM svv_columns
|
FROM svv_columns
|
||||||
WHERE table_schema NOT IN ('pg_internal','pg_catalog','information_schema')
|
WHERE table_schema NOT IN ('pg_internal','pg_catalog','information_schema')
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT DISTINCT view_name::varchar AS table_name,
|
SELECT DISTINCT view_name::varchar AS table_name,
|
||||||
view_schema::varchar AS table_schema,
|
view_schema::varchar AS table_schema,
|
||||||
col_name::varchar AS column_name
|
col_name::varchar AS column_name,
|
||||||
|
col_num AS pos
|
||||||
FROM pg_get_late_binding_view_cols()
|
FROM pg_get_late_binding_view_cols()
|
||||||
cols(view_schema name, view_name name, col_name name, col_type varchar, col_num int);
|
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)
|
self._get_definitions(schema, query)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="d-flex flex-column justify-content-center align-items-center">
|
<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="d-flex flex-column justify-content-center align-items-center">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="text-center m-b-15">
|
<div class="text-center m-b-15">
|
||||||
<a href="/"><img src="/static/images/redash_icon_small.png"></a>
|
<a href="/"><img src="/static/images/redash_icon_small.png"></a>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/jquery.min.js"></script>
|
<script src="/static/js/jquery.min.js"></script>
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ extract_redash_sources() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
install_python_packages() {
|
install_python_packages() {
|
||||||
pip install --upgrade pip
|
pip install --upgrade pip==9.0.3
|
||||||
# TODO: venv?
|
# TODO: venv?
|
||||||
pip install setproctitle # setproctitle is used by Celery for "pretty" process titles
|
pip install setproctitle # setproctitle is used by Celery for "pretty" process titles
|
||||||
pip install -r $REDASH_BASE_PATH/current/requirements.txt
|
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)
|
rv = self.make_request('post', '/api/widgets', data=data)
|
||||||
|
|
||||||
self.assertEquals(rv.status_code, 200)
|
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):
|
def test_delete_widget(self):
|
||||||
widget = self.factory.create_widget()
|
widget = self.factory.create_widget()
|
||||||
|
|||||||
@@ -1,94 +1,87 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const webpack = require('webpack');
|
const webpack = require("webpack");
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
const ExtractTextPlugin = require("extract-text-webpack-plugin");
|
const ExtractTextPlugin = require("extract-text-webpack-plugin");
|
||||||
const WebpackBuildNotifierPlugin = require('webpack-build-notifier');
|
const WebpackBuildNotifierPlugin = require("webpack-build-notifier");
|
||||||
const ManifestPlugin = require('webpack-manifest-plugin');
|
const ManifestPlugin = require("webpack-manifest-plugin");
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const LessPluginAutoPrefix = require('less-plugin-autoprefix');
|
const LessPluginAutoPrefix = require("less-plugin-autoprefix");
|
||||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
||||||
const path = require('path');
|
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 basePath = fs.realpathSync(path.join(__dirname, "client"));
|
||||||
const appPath = fs.realpathSync(path.join(__dirname, 'client', 'app'));
|
const appPath = fs.realpathSync(path.join(__dirname, "client", "app"));
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
entry: {
|
entry: {
|
||||||
app: [
|
app: ["./client/app/index.js", "./client/app/assets/less/main.less"],
|
||||||
'./client/app/index.js',
|
server: ["./client/app/assets/less/server.less"]
|
||||||
'./client/app/assets/less/main.less',
|
|
||||||
],
|
|
||||||
server: [
|
|
||||||
'./client/app/assets/less/server.less',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.join(basePath, './dist'),
|
path: path.join(basePath, "./dist"),
|
||||||
filename: '[name].js',
|
filename: "[name].js",
|
||||||
publicPath: '/static/'
|
publicPath: "/static/"
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': appPath,
|
"@": appPath,
|
||||||
// Currently `lodash` is used only by `gridstack.js`, but it can work
|
// 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
|
// with `underscore` as well, so set an alias to avoid bundling both `lodash` and
|
||||||
// `underscore`. When adding new libraries, check if they can work
|
// `underscore`. When adding new libraries, check if they can work
|
||||||
// with `underscore`, otherwise remove this line
|
// with `underscore`, otherwise remove this line
|
||||||
'lodash': 'underscore',
|
lodash: "underscore"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new WebpackBuildNotifierPlugin({title: 'Redash'}),
|
new WebpackBuildNotifierPlugin({ title: "Redash" }),
|
||||||
new webpack.DefinePlugin({
|
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
|
// 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`)
|
// bundle only default `moment` locale (`en`)
|
||||||
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
|
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
|
||||||
new webpack.optimize.CommonsChunkPlugin({
|
new webpack.optimize.CommonsChunkPlugin({
|
||||||
name: 'vendor',
|
name: "vendor",
|
||||||
minChunks: function (module, count) {
|
minChunks: function(module, count) {
|
||||||
// any required modules inside node_modules are extracted to vendor
|
// any required modules inside node_modules are extracted to vendor
|
||||||
return (
|
return (
|
||||||
module.resource &&
|
module.resource &&
|
||||||
/\.js$/.test(module.resource) &&
|
/\.js$/.test(module.resource) &&
|
||||||
module.resource.indexOf(
|
module.resource.indexOf(path.join(__dirname, "./node_modules")) === 0
|
||||||
path.join(__dirname, './node_modules')
|
);
|
||||||
) === 0
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
// extract webpack runtime and module manifest to its own file in order to
|
// extract webpack runtime and module manifest to its own file in order to
|
||||||
// prevent vendor hash from being updated whenever app bundle is updated
|
// prevent vendor hash from being updated whenever app bundle is updated
|
||||||
new webpack.optimize.CommonsChunkPlugin({
|
new webpack.optimize.CommonsChunkPlugin({
|
||||||
name: 'manifest',
|
name: "manifest",
|
||||||
chunks: ['vendor']
|
chunks: ["vendor"]
|
||||||
}),
|
}),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: './client/app/index.html',
|
template: "./client/app/index.html",
|
||||||
filename: 'index.html',
|
filename: "index.html",
|
||||||
excludeChunks: ['server'],
|
excludeChunks: ["server"]
|
||||||
}),
|
}),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: './client/app/multi_org.html',
|
template: "./client/app/multi_org.html",
|
||||||
filename: 'multi_org.html',
|
filename: "multi_org.html",
|
||||||
excludeChunks: ['server'],
|
excludeChunks: ["server"]
|
||||||
}),
|
}),
|
||||||
new ExtractTextPlugin({
|
new ExtractTextPlugin({
|
||||||
filename: '[name].[chunkhash].css',
|
filename: "[name].[chunkhash].css"
|
||||||
}),
|
}),
|
||||||
new ManifestPlugin({
|
new ManifestPlugin({
|
||||||
fileName: 'asset-manifest.json'
|
fileName: "asset-manifest.json"
|
||||||
}),
|
}),
|
||||||
new CopyWebpackPlugin([
|
new CopyWebpackPlugin([
|
||||||
{ from: 'client/app/assets/robots.txt' },
|
{ from: "client/app/assets/robots.txt" },
|
||||||
{ from: 'client/app/assets/css/login.css', to: 'styles/login.css' },
|
{ 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: "node_modules/jquery/dist/jquery.min.js", to: "js/jquery.min.js" }
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -97,113 +90,122 @@ const config = {
|
|||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
use: ['babel-loader', 'eslint-loader']
|
use: ["babel-loader", "eslint-loader"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.html$/,
|
test: /\.html$/,
|
||||||
exclude: [/node_modules/, /index\.html/],
|
exclude: [/node_modules/, /index\.html/],
|
||||||
use: [{
|
use: [
|
||||||
loader: 'raw-loader'
|
{
|
||||||
}]
|
loader: "raw-loader"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: ExtractTextPlugin.extract([{
|
use: ExtractTextPlugin.extract([
|
||||||
loader: 'css-loader',
|
{
|
||||||
|
loader: "css-loader",
|
||||||
options: {
|
options: {
|
||||||
minimize: process.env.NODE_ENV === 'production'
|
minimize: process.env.NODE_ENV === "production"
|
||||||
}
|
}
|
||||||
}])
|
}
|
||||||
|
])
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.less$/,
|
test: /\.less$/,
|
||||||
use: ExtractTextPlugin.extract([
|
use: ExtractTextPlugin.extract([
|
||||||
{
|
{
|
||||||
loader: 'css-loader',
|
loader: "css-loader",
|
||||||
options: {
|
options: {
|
||||||
minimize: process.env.NODE_ENV === 'production'
|
minimize: process.env.NODE_ENV === "production"
|
||||||
}
|
}
|
||||||
}, {
|
},
|
||||||
loader: 'less-loader',
|
{
|
||||||
|
loader: "less-loader",
|
||||||
options: {
|
options: {
|
||||||
plugins: [
|
plugins: [new LessPluginAutoPrefix({ browsers: ["last 3 versions"] })]
|
||||||
new LessPluginAutoPrefix({browsers: ['last 3 versions']})
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||||
use: [{
|
use: [
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
|
||||||
context: path.resolve(appPath, './assets/images/'),
|
|
||||||
outputPath: 'images/',
|
|
||||||
name: '[path][name].[ext]',
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
test: /\.geo\.json$/,
|
loader: "file-loader",
|
||||||
use: [{
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
options: {
|
||||||
outputPath: 'data/',
|
context: path.resolve(appPath, "./assets/images/"),
|
||||||
name: '[hash:7].[name].[ext]',
|
outputPath: "images/",
|
||||||
|
name: "[path][name].[ext]"
|
||||||
}
|
}
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
|
||||||
use: [{
|
|
||||||
loader: 'url-loader',
|
|
||||||
options: {
|
|
||||||
limit: 10000,
|
|
||||||
name: 'fonts/[name].[hash:7].[ext]'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
devtool: 'cheap-eval-module-source-map',
|
{
|
||||||
|
test: /\.geo\.json$/,
|
||||||
|
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]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
devtool: "cheap-eval-module-source-map",
|
||||||
stats: {
|
stats: {
|
||||||
modules: false,
|
modules: false,
|
||||||
chunkModules: false,
|
chunkModules: false
|
||||||
},
|
},
|
||||||
watchOptions: {
|
watchOptions: {
|
||||||
ignored: /\.sw.$/,
|
ignored: /\.sw.$/
|
||||||
},
|
},
|
||||||
devServer: {
|
devServer: {
|
||||||
inline: true,
|
inline: true,
|
||||||
index: '/static/index.html',
|
index: "/static/index.html",
|
||||||
historyApiFallback: {
|
historyApiFallback: {
|
||||||
index: '/static/index.html',
|
index: "/static/index.html",
|
||||||
rewrites: [{from: /./, to: '/static/index.html'}],
|
rewrites: [{ from: /./, to: "/static/index.html" }]
|
||||||
},
|
},
|
||||||
contentBase: false,
|
contentBase: false,
|
||||||
publicPath: '/static/',
|
publicPath: "/static/",
|
||||||
proxy: [
|
proxy: [
|
||||||
{
|
{
|
||||||
context: ['/login', '/logout', '/invite', '/setup', '/status.json', '/api', '/oauth'],
|
context: ["/login", "/logout", "/invite", "/setup", "/status.json", "/api", "/oauth"],
|
||||||
target: redashBackend + '/',
|
target: redashBackend + "/",
|
||||||
changeOrigin: true,
|
changeOrigin: false,
|
||||||
secure: false,
|
secure: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
context: (path) => {
|
context: path => {
|
||||||
// CSS/JS for server-rendered pages should be served from backend
|
// CSS/JS for server-rendered pages should be served from backend
|
||||||
return /^\/static\/[a-z]+\.[0-9a-fA-F]+\.(css|js)$/.test(path);
|
return /^\/static\/[a-z]+\.[0-9a-fA-F]+\.(css|js)$/.test(path);
|
||||||
},
|
},
|
||||||
target: redashBackend + '/',
|
target: redashBackend + "/",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
stats: {
|
stats: {
|
||||||
modules: false,
|
modules: false,
|
||||||
chunkModules: false,
|
chunkModules: false
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -211,15 +213,17 @@ if (process.env.DEV_SERVER_HOST) {
|
|||||||
config.devServer.host = process.env.DEV_SERVER_HOST;
|
config.devServer.host = process.env.DEV_SERVER_HOST;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === "production") {
|
||||||
config.output.filename = '[name].[chunkhash].js';
|
config.output.filename = "[name].[chunkhash].js";
|
||||||
config.plugins.push(new webpack.optimize.UglifyJsPlugin({
|
config.plugins.push(
|
||||||
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
sourceMap: true,
|
sourceMap: true,
|
||||||
compress: {
|
compress: {
|
||||||
warnings: true
|
warnings: true
|
||||||
}
|
}
|
||||||
}));
|
})
|
||||||
config.devtool = 'source-map';
|
);
|
||||||
|
config.devtool = "source-map";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.BUNDLE_ANALYZER) {
|
if (process.env.BUNDLE_ANALYZER) {
|
||||||
|
|||||||
Reference in New Issue
Block a user