Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Parameters on Public Dashboards #3659

Merged
merged 94 commits into from
Jul 15, 2019
Merged
Show file tree
Hide file tree
Changes from 73 commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
8871dc7
change has_access and require_access signatures to work with the obje…
Mar 19, 2019
b8ec83f
use the textless endpoint (/api/queries/:id/results) for pristine
Jan 30, 2019
c51bc40
Revert "use the textless endpoint (/api/queries/:id/results) for pris…
Feb 3, 2019
b5001c1
go to textless /api/queries/:id/results by default
Feb 3, 2019
ec3a25d
change `run_query`'s signature to accept a ParameterizedQuery instead of
Feb 3, 2019
70f46f1
raise HTTP 400 when receiving invalid parameter values. Fixes #3394
Feb 6, 2019
f7d67f6
enqueue jobs for ApiUsers
Feb 26, 2019
c9abc77
rename `id` to `user_id`
Mar 3, 2019
6823d5a
support executing queries using Query api_keys by instantiating an Ap…
Mar 4, 2019
3f49053
show deprecation messages for ALLOW_PARAMETERS_IN_EMBEDS. Also, move
Mar 4, 2019
c97f1cc
add link to forum message regarding embed deprecation
Mar 5, 2019
cac3e4b
change API to /api/queries/:id/dropdowns/:dropdown_id
Mar 6, 2019
ef8e0e0
split to 2 different dropdown endpoints and implement the second
Mar 6, 2019
c355f7c
add test cases for /api/queries/:id/dropdowns/:id
Mar 6, 2019
0a335b9
use new /dropdowns endpoint in frontend
Mar 7, 2019
10306b7
first e2e test for sharing embeds
Mar 11, 2019
1a554d7
Pleasing the CodeClimate overlords
Mar 11, 2019
37af437
All glory to CodeClimate
Mar 18, 2019
93878e6
remove residues from bad rebase
Mar 24, 2019
9c63f21
add query id and data source id to serialized public dashboards
Mar 24, 2019
1ced01c
add global parameters directive to public dashboards page
Mar 24, 2019
e87733b
allow access to a query by the api_key of the dashboard which includes
Mar 26, 2019
0298454
rename `object` to `obj`
Mar 27, 2019
68870c6
simplify permission tests once `has_access` accepts groups
Mar 27, 2019
a99902a
support global parameters for public dashboards
Mar 28, 2019
9c67e8a
change has_access and require_access signatures to work with the obje…
Mar 19, 2019
2188d5b
rename `object` to `obj`
Mar 27, 2019
39a2074
simplify permission tests once `has_access` accepts groups
Mar 27, 2019
7307d31
no need to log `is_api_key`
Mar 28, 2019
68dd7e7
Merge branch 'master' into allow-parameters-on-dashboards
Apr 2, 2019
19104bd
send parameters to public dashboard page
Apr 3, 2019
b42671e
allow access to a query by the api_key of the dashboard which include…
Apr 3, 2019
9748a38
disable sharing if dashboard is associated with unsafe queries
Apr 3, 2019
6be7e14
remove cypress test added in the wrong place due to a faulty rebase
Apr 3, 2019
a5fdd85
add support for clicking buttons in cy.clickThrough
Apr 4, 2019
18dcc73
Cypress test which verifies that dashboards with safe queries can be …
Apr 4, 2019
4e03843
Cypress test which verifies that dashboards with unsafe queries can't…
Apr 4, 2019
e2a8944
Merge branch 'master' into allow-parameters-on-dashboards
Apr 4, 2019
80dafae
remove duplicate tests
Apr 6, 2019
2ae7c87
Merge branch 'master' into allow-parameters-on-dashboards
Apr 7, 2019
fd8c090
Merge branch 'master' into allow-parameters-on-dashboards
Apr 10, 2019
dead664
use this.enabled and negate when needed
Apr 10, 2019
716c9e8
remove stale comment
Apr 10, 2019
5bbe878
add another Cypress test to verify that unauthenticated users have ac…
Apr 10, 2019
7cc1371
obviously, I commit 'only' the first time I use it
Apr 10, 2019
6296d04
Merge branch 'master' into allow-parameters-on-dashboards
Apr 14, 2019
f838030
search for query access by query id and not api_key
Apr 14, 2019
fc7323d
no need to fetch latest query data as it is loaded by frontend from t…
Apr 14, 2019
4f742fb
test that queries associated with dashboards are accessible when supp…
Apr 15, 2019
5a782cb
propagate `isDirty` down to `QueryBasedParameterInput`
Apr 17, 2019
bc48a23
go to /api/:id/dropdown while editing a query, since dropdown queries…
Apr 17, 2019
6e3d6f0
show helpful error message if dropdown values cannot be fetched
Apr 17, 2019
0bed9d5
Merge branch 'master' into fix-add-query-based-parameter-to-existing-…
Apr 28, 2019
144ec38
Merge branch 'master' into fix-add-query-based-parameter-to-existing-…
May 4, 2019
3512575
use backticks instead of line concatenation
May 4, 2019
d00e32e
remove requirement to have direct access to dropdown query in order v…
May 5, 2019
e2c801c
remove isDirty-based implementation and allow dropdown queries throug…
May 5, 2019
429c8d9
fix tests to cover all cases for /api/queries/:id/dropdowns/:id
May 5, 2019
797520c
Merge branch 'master' into allow-parameters-on-dashboards
May 6, 2019
2cba5db
Merge branch 'master' into fix-add-query-based-parameter-to-existing-…
May 6, 2019
cd2a2df
fix indentation
May 6, 2019
299b3e1
Merge branch 'fix-add-query-based-parameter-to-existing-queries' into…
May 6, 2019
44cb9b5
require access to the query, not the data source
May 6, 2019
6b0487c
Merge branch 'fix-add-query-based-parameter-to-existing-queries' into…
May 6, 2019
821762d
Merge branch 'master' into allow-parameters-on-dashboards
May 6, 2019
e0dc262
resolve dashboard user by query id
May 6, 2019
19a2bc6
Merge branch 'master' into allow-parameters-on-dashboards
May 12, 2019
e20b46e
Merge branch 'master' into allow-parameters-on-dashboards
May 12, 2019
4ebffe3
Merge branch 'allow-parameters-on-dashboards' of github.com:getredash…
May 12, 2019
c4cf96b
apply new copy to Cypress tests
May 12, 2019
86a4c7a
if only something would have prevented me from commiting an 'only' ca…
May 12, 2019
6714baf
very important handling of whitespace
May 12, 2019
ca72838
Merge branch 'master' into allow-parameters-on-dashboards
May 13, 2019
e62876d
Merge branch 'master' into allow-parameters-on-dashboards
May 29, 2019
f41efb8
respond to parameter's Apply button
May 30, 2019
bc47de5
Merge branch 'master' into allow-parameters-on-dashboards
Jun 25, 2019
c00c720
text widgets are safe for sharing
Jun 26, 2019
f259fde
remove redundant event
Jun 26, 2019
4098cf2
add a safety check that object has dashboard_api_keys before calling it
Jun 26, 2019
615325f
supply a parameter value for text parameters to have it show up
Jun 26, 2019
bad54fd
add parameter values for date and datetime
Jun 26, 2019
cdbf91e
use the current year and month to avoid pagination
Jun 26, 2019
8510745
use Cypress.moment() instead of preinstalled moment()
Jun 27, 2019
89a854f
Merge branch 'parameter-spec-fixes' into allow-parameters-on-dashboards
Jun 27, 2019
865d22e
Merge branch 'master' into allow-parameters-on-dashboards
Jul 1, 2019
9646cd4
explicitly create parameters
Jul 1, 2019
500c155
refresh query data if a querystring parameter is provided
Jul 11, 2019
17fa5e0
avoid sending a data_source_id - it's only relevant to unsaved querie…
Jul 11, 2019
16be2f6
remove empty query text workaround
Jul 11, 2019
64248d4
provide default value to parameter
Jul 11, 2019
4ad3787
add a few more dashboard sharing specs
Jul 11, 2019
c62e575
lint
Jul 14, 2019
c2206f0
wait for DynamicTable to appear to reveal that actual results are dis…
Jul 14, 2019
fd874ca
override error message for unsafely shared widgets
Jul 15, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions client/app/pages/dashboards/ShareDashboardDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const API_SHARE_URL = 'api/dashboards/{id}/share';
class ShareDashboardDialog extends React.Component {
static propTypes = {
dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
hasQueryParams: PropTypes.bool.isRequired,
hasOnlySafeQueries: PropTypes.bool.isRequired,
dialog: DialogPropType.isRequired,
};

Expand All @@ -35,7 +35,7 @@ class ShareDashboardDialog extends React.Component {
};

this.apiUrl = replace(API_SHARE_URL, '{id}', dashboard.id);
this.disabled = this.props.hasQueryParams && !dashboard.publicAccessEnabled;
this.enabled = this.props.hasOnlySafeQueries || dashboard.publicAccessEnabled;
}

static get headerContent() {
Expand Down Expand Up @@ -104,10 +104,10 @@ class ShareDashboardDialog extends React.Component {
footer={null}
>
<Form layout="horizontal">
{this.props.hasQueryParams && (
{!this.props.hasOnlySafeQueries && (
<Form.Item>
<Alert
message="Sharing is currently not supported for dashboards containing queries with parameters."
message="For your security, sharing is currently not supported for dashboards containing queries with text parameters. Consider changing the text parameters in your query to a different type."
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
type="error"
/>
</Form.Item>
Expand All @@ -117,12 +117,13 @@ class ShareDashboardDialog extends React.Component {
checked={dashboard.publicAccessEnabled}
onChange={this.onChange}
loading={this.state.saving}
disabled={this.disabled}
disabled={!this.enabled}
data-test="PublicAccessEnabled"
/>
</Form.Item>
{dashboard.public_url && (
<Form.Item label="Secret address" {...this.formItemProps}>
<InputWithCopy value={dashboard.public_url} />
<InputWithCopy value={dashboard.public_url} data-test="SecretAddress" />
</Form.Item>
)}
</Form>
Expand Down
2 changes: 1 addition & 1 deletion client/app/pages/dashboards/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ <h3>
<button type="button" class="btn btn-sm hidden-xs" ng-class="{'btn-default': !$ctrl.isFullscreen, 'btn-primary': $ctrl.isFullscreen}" tooltip="Enable/Disable Fullscreen display" ng-click="$ctrl.toggleFullscreen()" ng-if="!$ctrl.dashboard.is_draft && !$ctrl.layoutEditing">
<span class="zmdi zmdi-fullscreen"></span>
</button>
<button type="button" class="btn btn-sm hidden-xs" ng-class="{'btn-default': !$ctrl.dashboard.publicAccessEnabled, 'btn-primary': $ctrl.dashboard.publicAccessEnabled}" tooltip="Enable/Disable Share URL" ng-click="$ctrl.openShareForm()" ng-if="($ctrl.dashboard.canEdit() || $ctrl.dashboard.publicAccessEnabled) && !$ctrl.dashboard.is_draft && !$ctrl.layoutEditing">
<button type="button" class="btn btn-sm hidden-xs" ng-class="{'btn-default': !$ctrl.dashboard.publicAccessEnabled, 'btn-primary': $ctrl.dashboard.publicAccessEnabled}" tooltip="Enable/Disable Share URL" ng-click="$ctrl.openShareForm()" ng-if="($ctrl.dashboard.canEdit() || $ctrl.dashboard.publicAccessEnabled) && !$ctrl.dashboard.is_draft && !$ctrl.layoutEditing" data-test="OpenShareForm">
<span class="zmdi zmdi-share"></span>
</button>
</span>
Expand Down
7 changes: 3 additions & 4 deletions client/app/pages/dashboards/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,15 +428,14 @@ function DashboardCtrl(
}

this.openShareForm = () => {
// check if any of the wigets have query parameters
const hasQueryParams = _.some(
const hasOnlySafeQueries = _.every(
this.dashboard.widgets,
w => !_.isEmpty(w.getQuery() && w.getQuery().getParametersDefs()),
w => w.getQuery() && w.getQuery().is_safe,
arikfr marked this conversation as resolved.
Show resolved Hide resolved
);

ShareDashboardDialog.showModal({
dashboard: this.dashboard,
hasQueryParams,
hasOnlySafeQueries,
});
};
}
Expand Down
6 changes: 5 additions & 1 deletion client/app/pages/dashboards/public-dashboard-page.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<div class="container p-t-10 p-b-20">
<div class="container p-t-10 p-b-20" ng-if="$ctrl.dashboard" data-test="PublicDashboard">
<page-header title="$ctrl.dashboard.name"></page-header>

<div class="m-b-10 p-15 bg-white tiled" ng-if="$ctrl.globalParameters.length > 0">
<parameters parameters="$ctrl.globalParameters"></parameters>
</div>

<div class="m-b-5">
<filters filters="$ctrl.filters" on-change="$ctrl.filtersOnChange"></filters>
</div>
Expand Down
48 changes: 25 additions & 23 deletions client/app/pages/dashboards/public-dashboard-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,40 +22,43 @@ const PublicDashboardPage = {

this.logoUrl = logoUrl;
this.public = true;
this.dashboard.widgets = Dashboard.prepareDashboardWidgets(this.dashboard.widgets);
this.globalParameters = [];

const refreshRate = Math.max(30, parseFloat($location.search().refresh));
this.extractGlobalParameters = () => {
this.globalParameters = this.dashboard.getParametersDefs();
};

if (refreshRate) {
const refresh = () => {
loadDashboard($http, $route).then((data) => {
this.dashboard = data;
this.dashboard.widgets = Dashboard.prepareDashboardWidgets(this.dashboard.widgets);
$scope.$on('dashboard.update-parameters', () => {
arikfr marked this conversation as resolved.
Show resolved Hide resolved
this.extractGlobalParameters();
});

this.filters = []; // TODO: implement (@/services/dashboard.js:collectDashboardFilters)
this.filtersOnChange = (allFilters) => {
this.filters = allFilters;
$scope.$applyAsync();
};
const refreshRate = Math.max(30, parseFloat($location.search().refresh));

const refresh = () => {
loadDashboard($http, $route).then((data) => {
this.dashboard = new Dashboard(data);
this.dashboard.widgets = Dashboard.prepareDashboardWidgets(this.dashboard.widgets);
this.filters = []; // TODO: implement (@/services/dashboard.js:collectDashboardFilters)
this.filtersOnChange = (allFilters) => {
this.filters = allFilters;
$scope.$applyAsync();
};

$timeout(refresh, refreshRate * 1000.0);
});
};
this.extractGlobalParameters();
});
};

if (refreshRate) {
$timeout(refresh, refreshRate * 1000.0);
}

refresh();
},
};

export default function init(ngModule) {
ngModule.component('publicDashboardPage', PublicDashboardPage);

function loadPublicDashboard($http, $route) {
'ngInject';

return loadDashboard($http, $route);
}

function session($http, $route, Auth) {
const token = $route.current.params.token;
Auth.setApiKey(token);
Expand All @@ -64,10 +67,9 @@ export default function init(ngModule) {

ngModule.config(($routeProvider) => {
$routeProvider.when('/public/dashboards/:token', {
template: '<public-dashboard-page dashboard="$resolve.dashboard"></public-dashboard-page>',
template: '<public-dashboard-page></public-dashboard-page>',
reloadOnSearch: false,
resolve: {
dashboard: loadPublicDashboard,
session,
},
});
Expand Down
3 changes: 1 addition & 2 deletions client/app/services/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ function DashboardService($resource, $http, $location, currentUser, Widget, dash

resource.prepareDashboardWidgets = prepareDashboardWidgets;
resource.prepareWidgetsForDashboard = prepareWidgetsForDashboard;

resource.prototype.getParametersDefs = function getParametersDefs() {
const globalParams = {};
const queryParams = $location.search();
Expand All @@ -190,7 +189,7 @@ function DashboardService($resource, $http, $location, currentUser, Widget, dash
const mappings = widget.getParameterMappings();
widget
.getQuery()
.getParametersDefs()
.getParametersDefs(false)
.forEach((param) => {
const mapping = mappings[param.name];
if (mapping.type === Widget.MappingType.DashboardLevel) {
Expand Down
12 changes: 6 additions & 6 deletions client/app/services/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,13 @@ class Parameters {
return parameters;
}

updateParameters() {
updateParameters(update) {
if (this.query.query === this.cachedQueryText) {
return;
}

this.cachedQueryText = this.query.query;
const parameterNames = this.parseQuery();
const parameterNames = update ? this.parseQuery() : map(this.query.options.parameters, p => p.name);

this.query.options.parameters = this.query.options.parameters || [];

Expand Down Expand Up @@ -269,8 +269,8 @@ class Parameters {
});
}

get() {
this.updateParameters();
get(update = true) {
this.updateParameters(update);
return this.query.options.parameters;
}

Expand Down Expand Up @@ -576,8 +576,8 @@ function QueryResource(
return this.$parameters;
};

QueryService.prototype.getParametersDefs = function getParametersDefs() {
return this.getParameters().get();
QueryService.prototype.getParametersDefs = function getParametersDefs(update = true) {
return this.getParameters().get(update);
};

return QueryService;
Expand Down
2 changes: 1 addition & 1 deletion client/app/services/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ function WidgetFactory($http, $location, Query, dashboardGridOptions) {

const existingParams = {};
// textboxes does not have query
const params = this.getQuery() ? this.getQuery().getParametersDefs() : [];
const params = this.getQuery() ? this.getQuery().getParametersDefs(false) : [];
each(params, (param) => {
existingParams[param.name] = true;
if (!isObject(this.options.parameterMappings[param.name])) {
Expand Down
101 changes: 101 additions & 0 deletions client/cypress/integration/dashboard/dashboard_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,107 @@ describe('Dashboard', () => {
});
});

describe('Sharing', () => {
beforeEach(function () {
createDashboard('Foo Bar').then(({ slug, id }) => {
this.dashboardId = id;
this.dashboardUrl = `/dashboard/${slug}`;
});
});

it('is possible if all queries are safe', function () {
const options = {
parameters: [{
name: 'foo',
type: 'number',
}],
};

const dashboardUrl = this.dashboardUrl;
createQuery({ options }).then(({ id: queryId }) => {
cy.visit(dashboardUrl);
editDashboard();
cy.contains('a', 'Add Widget').click();
cy.getByTestId('AddWidgetDialog').within(() => {
cy.get(`.query-selector-result[data-test="QueryId${queryId}"]`).click();
});
cy.contains('button', 'Add to Dashboard').click();
cy.getByTestId('AddWidgetDialog').should('not.exist');
cy.clickThrough({ button: `
Done Editing
Publish
` },
`OpenShareForm
PublicAccessEnabled`);

cy.getByTestId('SecretAddress').should('exist');
});
});

it('is available to unauthenticated users', function () {
arikfr marked this conversation as resolved.
Show resolved Hide resolved
const options = {
parameters: [{
name: 'foo',
type: 'number',
}],
};

const dashboardUrl = this.dashboardUrl;
createQuery({ options }).then(({ id: queryId }) => {
cy.visit(dashboardUrl);
editDashboard();
cy.contains('a', 'Add Widget').click();
cy.getByTestId('AddWidgetDialog').within(() => {
cy.get(`.query-selector-result[data-test="QueryId${queryId}"]`).click();
});
cy.contains('button', 'Add to Dashboard').click();
cy.getByTestId('AddWidgetDialog').should('not.exist');
cy.clickThrough({ button: `
Done Editing
Publish
` },
`OpenShareForm
PublicAccessEnabled`);

cy.getByTestId('SecretAddress').invoke('val').then((secretAddress) => {
cy.logout();
cy.visit(secretAddress);
cy.getByTestId('PublicDashboard', { timeout: 10000 }).should('exist');
cy.percySnapshot('Successfully Shared Parameterized Dashboard');
});
});
});

it('is not possible if some queries are not safe', function () {
const options = {
parameters: [{
name: 'foo',
type: 'text',
}],
};

const dashboardUrl = this.dashboardUrl;
createQuery({ options }).then(({ id: queryId }) => {
cy.visit(dashboardUrl);
editDashboard();
cy.contains('a', 'Add Widget').click();
cy.getByTestId('AddWidgetDialog').within(() => {
cy.get(`.query-selector-result[data-test="QueryId${queryId}"]`).click();
});
cy.contains('button', 'Add to Dashboard').click();
cy.getByTestId('AddWidgetDialog').should('not.exist');
cy.clickThrough({ button: `
Done Editing
Publish
` },
'OpenShareForm');

cy.getByTestId('PublicAccessEnabled').should('be.disabled');
});
});
});


describe('Textbox', () => {
beforeEach(function () {
createDashboard('Foo Bar').then(({ slug, id }) => {
Expand Down
32 changes: 26 additions & 6 deletions client/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,31 @@ Cypress.Commands.add('login', (email = 'admin@redash.io', password = 'password')

Cypress.Commands.add('logout', () => cy.request('/logout'));
Cypress.Commands.add('getByTestId', element => cy.get('[data-test="' + element + '"]'));
Cypress.Commands.add('clickThrough', (elements) => {
elements
.trim()
.split(/\s/)
.filter(Boolean)
.forEach(element => cy.getByTestId(element).click());

/* Clicks a series of elements. Pass in a newline-seperated string in order to click all elements by their test id,
or enclose the above string in an object with 'button' as key to click the buttons by name. For example:

cy.clickThrough(`
TestId1
TestId2
TestId3
`, { button: `
Label of button 4
Label of button 5
` }, `
TestId6
TestId7`);
*/
Cypress.Commands.add('clickThrough', (...args) => {
args.forEach((elements) => {
const names = elements.button || elements;

const click = element => (elements.button ?
cy.contains('button', element.trim()) :
cy.getByTestId(element.trim())).click();

names.trim().split(/\n/).filter(Boolean).forEach(click);
});

return undefined;
});
12 changes: 12 additions & 0 deletions redash/handlers/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,18 @@ def messages():
return messages


def messages():
messages = []

if not current_user.is_email_verified:
messages.append('email-not-verified')

if settings.ALLOW_PARAMETERS_IN_EMBEDS:
messages.append('using-deprecated-embed-feature')

return messages


@routes.route('/api/config', methods=['GET'])
def config(org_slug=None):
return json_response({
Expand Down
Loading